This repository has been archived on 2026-04-03. You can view files and clone it. You cannot open issues or pull requests or push a commit.
Files
core/server/store/sqlite/provider.go
2026-03-19 18:50:27 +00:00

267 lines
8.6 KiB
Go

package sqlite
import (
"context"
"database/sql"
"encoding/json"
"fmt"
"time"
"chat-switchboard/models"
"chat-switchboard/store"
)
type ProviderStore struct{}
func NewProviderStore() *ProviderStore { return &ProviderStore{} }
const providerCols = `id, scope, owner_id, name, provider, endpoint, api_key_enc,
key_nonce, key_scope, model_default, config, headers, settings, is_active, is_private, created_at, updated_at`
func (s *ProviderStore) Create(ctx context.Context, cfg *models.ProviderConfig) error {
cfg.ID = store.NewID()
now := time.Now().UTC()
cfg.CreatedAt = now
cfg.UpdatedAt = now
_, err := DB.ExecContext(ctx, `
INSERT INTO provider_configs (id, scope, owner_id, name, provider, endpoint, api_key_enc,
key_nonce, key_scope, model_default, config, headers, settings, is_active, is_private,
created_at, updated_at)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
cfg.ID, cfg.Scope, models.NullString(cfg.OwnerID), cfg.Name, cfg.Provider, cfg.Endpoint,
cfg.APIKeyEnc, cfg.KeyNonce, cfg.KeyScope, cfg.ModelDefault,
ToJSON(cfg.Config), ToJSON(cfg.Headers), ToJSON(cfg.Settings),
cfg.IsActive, cfg.IsPrivate,
now.Format(timeFmt), now.Format(timeFmt),
)
return err
}
func (s *ProviderStore) GetByID(ctx context.Context, id string) (*models.ProviderConfig, error) {
row := DB.QueryRowContext(ctx,
fmt.Sprintf("SELECT %s FROM provider_configs WHERE id = ?", providerCols), id)
return scanProvider(row)
}
func (s *ProviderStore) Update(ctx context.Context, id string, patch models.ProviderConfigPatch) error {
b := NewUpdate("provider_configs")
if patch.Name != nil {
b.Set("name", *patch.Name)
}
if patch.Endpoint != nil {
b.Set("endpoint", *patch.Endpoint)
}
if patch.APIKeyEnc != nil {
b.Set("api_key_enc", patch.APIKeyEnc)
b.Set("key_nonce", patch.KeyNonce)
}
if patch.ModelDefault != nil {
b.Set("model_default", *patch.ModelDefault)
}
if patch.Config != nil {
b.SetJSON("config", patch.Config)
}
if patch.Headers != nil {
b.SetJSON("headers", patch.Headers)
}
if patch.Settings != nil {
b.SetJSON("settings", patch.Settings)
}
if patch.IsActive != nil {
b.Set("is_active", *patch.IsActive)
}
if patch.IsPrivate != nil {
b.Set("is_private", *patch.IsPrivate)
}
if !b.HasSets() {
return nil
}
b.Where("id", id)
_, err := b.Exec(DB)
return err
}
func (s *ProviderStore) Delete(ctx context.Context, id string) error {
_, err := DB.ExecContext(ctx, "DELETE FROM provider_configs WHERE id = ?", id)
return err
}
func (s *ProviderStore) ListGlobal(ctx context.Context) ([]models.ProviderConfig, error) {
return s.listByScope(ctx, models.ScopeGlobal, "")
}
func (s *ProviderStore) ListForTeam(ctx context.Context, teamID string) ([]models.ProviderConfig, error) {
return s.listByScope(ctx, models.ScopeTeam, teamID)
}
func (s *ProviderStore) ListForUser(ctx context.Context, userID string) ([]models.ProviderConfig, error) {
return s.listByScope(ctx, models.ScopePersonal, userID)
}
// ListAccessible returns all provider configs a user can access:
// global + team (for teams they belong to) + personal.
func (s *ProviderStore) ListAccessible(ctx context.Context, userID string) ([]models.ProviderConfig, error) {
rows, err := DB.QueryContext(ctx, fmt.Sprintf(`
SELECT %s FROM provider_configs
WHERE is_active = 1 AND (
scope = 'global'
OR (scope = 'personal' AND owner_id = ?)
OR (scope = 'team' AND owner_id IN (
SELECT team_id FROM team_members WHERE user_id = ?
))
)
ORDER BY scope, name`, providerCols), userID, userID)
if err != nil {
return nil, err
}
defer rows.Close()
return scanProviders(rows)
}
// UserCanAccess checks if a user can use a specific provider config.
func (s *ProviderStore) UserCanAccess(ctx context.Context, userID, configID string) (bool, error) {
var exists bool
err := DB.QueryRowContext(ctx, `
SELECT EXISTS(
SELECT 1 FROM provider_configs
WHERE id = ? AND is_active = 1 AND (
scope = 'global'
OR (scope = 'personal' AND owner_id = ?)
OR (scope = 'team' AND owner_id IN (
SELECT team_id FROM team_members WHERE user_id = ?
))
)
)`, configID, userID, userID).Scan(&exists)
return exists, err
}
// ── Internal helpers ────────────────────────
func (s *ProviderStore) listByScope(ctx context.Context, scope, ownerID string) ([]models.ProviderConfig, error) {
var rows *sql.Rows
var err error
if scope == models.ScopeGlobal {
rows, err = DB.QueryContext(ctx,
fmt.Sprintf("SELECT %s FROM provider_configs WHERE scope = ? AND is_active = 1 ORDER BY name", providerCols),
scope)
} else {
rows, err = DB.QueryContext(ctx,
fmt.Sprintf("SELECT %s FROM provider_configs WHERE scope = ? AND owner_id = ? AND is_active = 1 ORDER BY name", providerCols),
scope, ownerID)
}
if err != nil {
return nil, err
}
defer rows.Close()
return scanProviders(rows)
}
func scanProvider(row *sql.Row) (*models.ProviderConfig, error) {
var p models.ProviderConfig
var ownerID, modelDefault, keyScope sql.NullString
var configJSON, headersJSON, settingsJSON []byte
err := row.Scan(
&p.ID, &p.Scope, &ownerID, &p.Name, &p.Provider, &p.Endpoint,
&p.APIKeyEnc, &p.KeyNonce, &keyScope, &modelDefault,
&configJSON, &headersJSON, &settingsJSON,
&p.IsActive, &p.IsPrivate, st(&p.CreatedAt), st(&p.UpdatedAt),
)
if err != nil {
return nil, err
}
p.OwnerID = NullableStringPtr(ownerID)
p.ModelDefault = modelDefault.String
p.KeyScope = keyScope.String
json.Unmarshal(configJSON, &p.Config)
json.Unmarshal(headersJSON, &p.Headers)
json.Unmarshal(settingsJSON, &p.Settings)
return &p, nil
}
func scanProviders(rows *sql.Rows) ([]models.ProviderConfig, error) {
var result []models.ProviderConfig
for rows.Next() {
var p models.ProviderConfig
var ownerID, modelDefault, keyScope sql.NullString
var configJSON, headersJSON, settingsJSON []byte
err := rows.Scan(
&p.ID, &p.Scope, &ownerID, &p.Name, &p.Provider, &p.Endpoint,
&p.APIKeyEnc, &p.KeyNonce, &keyScope, &modelDefault,
&configJSON, &headersJSON, &settingsJSON,
&p.IsActive, &p.IsPrivate, st(&p.CreatedAt), st(&p.UpdatedAt),
)
if err != nil {
return nil, err
}
p.OwnerID = NullableStringPtr(ownerID)
p.ModelDefault = modelDefault.String
p.KeyScope = keyScope.String
json.Unmarshal(configJSON, &p.Config)
json.Unmarshal(headersJSON, &p.Headers)
json.Unmarshal(settingsJSON, &p.Settings)
result = append(result, p)
}
return result, rows.Err()
}
// ── CS4 additions (v0.29.0) ─────────────────────────────────────────────
func (s *ProviderStore) DeletePersonalByOwner(ctx context.Context, ownerID string) (int64, error) {
result, err := DB.ExecContext(ctx,
`DELETE FROM provider_configs WHERE scope = 'personal' AND owner_id = ?`, ownerID)
if err != nil {
return 0, err
}
return result.RowsAffected()
}
// ── CS6 additions (v0.29.0) ─────────────────────────────────────────────
func (s *ProviderStore) ListAllForTeam(ctx context.Context, teamID string) ([]models.ProviderConfig, error) {
rows, err := DB.QueryContext(ctx,
fmt.Sprintf("SELECT %s FROM provider_configs WHERE scope = 'team' AND owner_id = ? ORDER BY name", providerCols),
teamID)
if err != nil {
return nil, err
}
defer rows.Close()
return scanProviders(rows)
}
func (s *ProviderStore) DeleteByIDAndTeam(ctx context.Context, id, teamID string) (int64, error) {
res, err := DB.ExecContext(ctx,
`DELETE FROM provider_configs WHERE id = ? AND scope = 'team' AND owner_id = ?`,
id, teamID)
if err != nil {
return 0, err
}
return res.RowsAffected()
}
// ── CS7a additions (v0.29.0) ────────────────────────────────────────────
func (s *ProviderStore) FindFirstForUser(ctx context.Context, userID string) (string, error) {
var configID string
err := DB.QueryRowContext(ctx, `
SELECT id FROM provider_configs
WHERE is_active = 1 AND (
(scope = 'personal' AND owner_id = ?)
OR scope = 'global'
)
ORDER BY scope ASC, created_at ASC
LIMIT 1
`, userID).Scan(&configID)
return configID, err
}
func (s *ProviderStore) LoadAccessible(ctx context.Context, configID, userID string) (*models.ProviderConfig, error) {
row := DB.QueryRowContext(ctx, fmt.Sprintf(`
SELECT %s FROM provider_configs
WHERE id = ? AND is_active = 1
AND (scope = 'global'
OR (scope = 'personal' AND owner_id = ?)
OR (scope = 'team' AND owner_id IN (SELECT team_id FROM team_members WHERE user_id = ?)))
`, providerCols), configID, userID, userID)
return scanProvider(row)
}