267 lines
8.7 KiB
Go
267 lines
8.7 KiB
Go
package sqlite
|
|
|
|
import (
|
|
"context"
|
|
"database/sql"
|
|
"encoding/json"
|
|
"fmt"
|
|
"time"
|
|
|
|
"git.gobha.me/xcaliber/chat-switchboard/models"
|
|
"git.gobha.me/xcaliber/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)
|
|
}
|