Files

99 lines
3.4 KiB
Go
Raw Permalink Normal View History

2026-08-19 14:25:44 +12:00
package store
import (
"context"
"encoding/json"
"errors"
"fmt"
"strings"
"time"
"github.com/jackc/pgx/v5"
)
// IntegrationPolicyKey is the operator's global on/off switch for external services that
// have no configuration document of their own.
//
// It is deliberately *not* a second copy of every integration's switch. Sonarr and Radarr
// already store theirs in the arr integration policy and MDBList stores its own in the
// ratings settings; moving those here would mean a migration and, worse, a window in which
// two documents disagreed about whether a service was on. The rule is one stored truth per
// integration, kept where that integration's other configuration already lives — and this
// document is that home for the ones that have nowhere else, which today is Tracearr.
//
// The API's integrationEnabled/setIntegrationEnabled pair is the single reader and writer,
// so the console never has to know which document answers for which service.
const IntegrationPolicyKey = "integration_policy"
// IntegrationPolicy is a map of integration id to whether it is switched on.
//
// Absence means on. A household that upgrades into this feature has every service it had
// configured still working, which is the only safe reading: the alternative silently turns
// off recommendation imports on the day the console gained a switch for them.
type IntegrationPolicy struct {
Disabled map[string]bool `json:"disabled"`
UpdatedAt time.Time `json:"updatedAt"`
}
// Enabled reports whether one integration is switched on. A service nobody has ever
// touched has no entry, and no entry is on.
func (p IntegrationPolicy) Enabled(id string) bool {
return !p.Disabled[strings.TrimSpace(id)]
}
func (s *Store) IntegrationPolicy(ctx context.Context) (IntegrationPolicy, error) {
empty := IntegrationPolicy{Disabled: map[string]bool{}}
var raw []byte
err := s.pool.QueryRow(ctx,
`SELECT value FROM app_settings WHERE key = $1`, IntegrationPolicyKey).Scan(&raw)
if errors.Is(err, pgx.ErrNoRows) {
return empty, nil
}
if err != nil {
return empty, fmt.Errorf("store: read integration policy: %w", err)
}
var policy IntegrationPolicy
if err := json.Unmarshal(raw, &policy); err != nil {
return empty, fmt.Errorf("store: decode integration policy: %w", err)
}
if policy.Disabled == nil {
policy.Disabled = map[string]bool{}
}
return policy, nil
}
// SetIntegrationEnabled records one service's switch, leaving every other entry alone.
//
// Read-modify-write rather than a whole-document put, because the console sends one
// switch at a time and a put would let a page rendered before another integration existed
// silently re-enable it.
func (s *Store) SetIntegrationEnabled(ctx context.Context, id string, enabled bool) error {
id = strings.TrimSpace(id)
if id == "" {
return fmt.Errorf("store: integration policy needs an id")
}
policy, err := s.IntegrationPolicy(ctx)
if err != nil {
return err
}
if enabled {
delete(policy.Disabled, id)
} else {
policy.Disabled[id] = true
}
policy.UpdatedAt = time.Now().UTC()
raw, err := json.Marshal(policy)
if err != nil {
return 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()`,
IntegrationPolicyKey, string(raw))
if err != nil {
return fmt.Errorf("store: write integration policy: %w", err)
}
return nil
}