202 lines
6.6 KiB
Go
202 lines
6.6 KiB
Go
package sqlite
|
|
|
|
import (
|
|
"context"
|
|
"database/sql"
|
|
"encoding/json"
|
|
"time"
|
|
|
|
"git.gobha.me/xcaliber/chat-switchboard/models"
|
|
"git.gobha.me/xcaliber/chat-switchboard/store"
|
|
)
|
|
|
|
type ExtensionStore struct{}
|
|
|
|
func NewExtensionStore() *ExtensionStore { return &ExtensionStore{} }
|
|
|
|
func (s *ExtensionStore) Create(ctx context.Context, ext *models.Extension) error {
|
|
ext.ID = store.NewID()
|
|
now := time.Now().UTC()
|
|
ext.CreatedAt = now
|
|
ext.UpdatedAt = now
|
|
_, err := DB.ExecContext(ctx, `
|
|
INSERT INTO extensions (id, ext_id, name, version, tier, description, author,
|
|
manifest, is_system, is_enabled, scope, team_id, installed_by, created_at, updated_at)
|
|
VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)`,
|
|
ext.ID, ext.ExtID, ext.Name, ext.Version, ext.Tier, ext.Description, ext.Author,
|
|
ext.Manifest, ext.IsSystem, ext.IsEnabled, ext.Scope,
|
|
models.NullString(ext.TeamID), models.NullString(ext.InstalledBy),
|
|
now.Format(timeFmt), now.Format(timeFmt),
|
|
)
|
|
return err
|
|
}
|
|
|
|
func (s *ExtensionStore) GetByID(ctx context.Context, id string) (*models.Extension, error) {
|
|
return s.scanOne(ctx, `SELECT * FROM extensions WHERE id = ?`, id)
|
|
}
|
|
|
|
func (s *ExtensionStore) GetByExtID(ctx context.Context, extID string) (*models.Extension, error) {
|
|
return s.scanOne(ctx, `SELECT * FROM extensions WHERE ext_id = ?`, extID)
|
|
}
|
|
|
|
func (s *ExtensionStore) Update(ctx context.Context, id string, ext *models.Extension) error {
|
|
_, err := DB.ExecContext(ctx, `
|
|
UPDATE extensions SET
|
|
name = ?, version = ?, description = ?, author = ?,
|
|
manifest = ?, is_system = ?, is_enabled = ?,
|
|
updated_at = datetime('now')
|
|
WHERE id = ?`,
|
|
ext.Name, ext.Version, ext.Description, ext.Author,
|
|
ext.Manifest, ext.IsSystem, ext.IsEnabled, id,
|
|
)
|
|
return err
|
|
}
|
|
|
|
func (s *ExtensionStore) Delete(ctx context.Context, id string) error {
|
|
_, err := DB.ExecContext(ctx, `DELETE FROM extensions WHERE id = ?`, id)
|
|
return err
|
|
}
|
|
|
|
func (s *ExtensionStore) ListAll(ctx context.Context) ([]models.Extension, error) {
|
|
return s.scanMany(ctx, `SELECT * FROM extensions ORDER BY name`)
|
|
}
|
|
|
|
func (s *ExtensionStore) ListEnabled(ctx context.Context) ([]models.Extension, error) {
|
|
return s.scanMany(ctx, `SELECT * FROM extensions WHERE is_enabled = 1 ORDER BY name`)
|
|
}
|
|
|
|
// ListForUser returns all enabled extensions with user-specific overrides merged in.
|
|
// System extensions are always included (users can't disable them).
|
|
// Non-system extensions respect per-user enabled toggle.
|
|
func (s *ExtensionStore) ListForUser(ctx context.Context, userID string) ([]models.UserExtension, error) {
|
|
rows, err := DB.QueryContext(ctx, `
|
|
SELECT e.id, e.ext_id, e.name, e.version, e.tier, e.description, e.author,
|
|
e.manifest, e.is_system, e.is_enabled, e.scope, e.team_id, e.installed_by,
|
|
e.created_at, e.updated_at,
|
|
eus.is_enabled AS user_enabled,
|
|
eus.settings AS user_settings
|
|
FROM extensions e
|
|
LEFT JOIN extension_user_settings eus
|
|
ON eus.extension_id = e.id AND eus.user_id = ?
|
|
WHERE e.is_enabled = 1
|
|
AND (e.is_system = 1 OR COALESCE(eus.is_enabled, 1) = 1)
|
|
ORDER BY e.name`, userID)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer rows.Close()
|
|
|
|
var result []models.UserExtension
|
|
for rows.Next() {
|
|
var ue models.UserExtension
|
|
var teamID, installedBy sql.NullString
|
|
var userEnabled sql.NullBool
|
|
var userSettings []byte
|
|
|
|
if err := rows.Scan(
|
|
&ue.ID, &ue.ExtID, &ue.Name, &ue.Version, &ue.Tier,
|
|
&ue.Description, &ue.Author, &ue.Manifest, &ue.IsSystem,
|
|
&ue.IsEnabled, &ue.Scope, &teamID, &installedBy,
|
|
st(&ue.CreatedAt), st(&ue.UpdatedAt),
|
|
&userEnabled, &userSettings,
|
|
); err != nil {
|
|
return nil, err
|
|
}
|
|
ue.TeamID = NullableStringPtr(teamID)
|
|
ue.InstalledBy = NullableStringPtr(installedBy)
|
|
if userEnabled.Valid {
|
|
ue.UserEnabled = &userEnabled.Bool
|
|
}
|
|
if userSettings != nil {
|
|
raw := json.RawMessage(userSettings)
|
|
ue.UserSettings = &raw
|
|
}
|
|
result = append(result, ue)
|
|
}
|
|
if result == nil {
|
|
result = make([]models.UserExtension, 0)
|
|
}
|
|
return result, rows.Err()
|
|
}
|
|
|
|
func (s *ExtensionStore) GetUserSettings(ctx context.Context, extID, userID string) (*models.ExtensionUserSettings, error) {
|
|
var eus models.ExtensionUserSettings
|
|
err := DB.QueryRowContext(ctx, `
|
|
SELECT extension_id, user_id, settings, is_enabled
|
|
FROM extension_user_settings
|
|
WHERE extension_id = ? AND user_id = ?`,
|
|
extID, userID,
|
|
).Scan(&eus.ExtensionID, &eus.UserID, &eus.Settings, &eus.IsEnabled)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return &eus, nil
|
|
}
|
|
|
|
func (s *ExtensionStore) SetUserSettings(ctx context.Context, eus *models.ExtensionUserSettings) error {
|
|
_, err := DB.ExecContext(ctx, `
|
|
INSERT INTO extension_user_settings (extension_id, user_id, settings, is_enabled)
|
|
VALUES (?, ?, ?, ?)
|
|
ON CONFLICT (extension_id, user_id)
|
|
DO UPDATE SET settings = EXCLUDED.settings, is_enabled = EXCLUDED.is_enabled`,
|
|
eus.ExtensionID, eus.UserID, eus.Settings, eus.IsEnabled,
|
|
)
|
|
return err
|
|
}
|
|
|
|
func (s *ExtensionStore) DeleteUserSettings(ctx context.Context, extID, userID string) error {
|
|
_, err := DB.ExecContext(ctx, `
|
|
DELETE FROM extension_user_settings WHERE extension_id = ? AND user_id = ?`,
|
|
extID, userID,
|
|
)
|
|
return err
|
|
}
|
|
|
|
// ── Internal helpers ────────────────────────────
|
|
|
|
func (s *ExtensionStore) scanOne(ctx context.Context, query string, args ...interface{}) (*models.Extension, error) {
|
|
var ext models.Extension
|
|
var teamID, installedBy sql.NullString
|
|
err := DB.QueryRowContext(ctx, query, args...).Scan(
|
|
&ext.ID, &ext.ExtID, &ext.Name, &ext.Version, &ext.Tier,
|
|
&ext.Description, &ext.Author, &ext.Manifest, &ext.IsSystem,
|
|
&ext.IsEnabled, &ext.Scope, &teamID, &installedBy,
|
|
st(&ext.CreatedAt), st(&ext.UpdatedAt),
|
|
)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
ext.TeamID = NullableStringPtr(teamID)
|
|
ext.InstalledBy = NullableStringPtr(installedBy)
|
|
return &ext, nil
|
|
}
|
|
|
|
func (s *ExtensionStore) scanMany(ctx context.Context, query string, args ...interface{}) ([]models.Extension, error) {
|
|
rows, err := DB.QueryContext(ctx, query, args...)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer rows.Close()
|
|
|
|
var result []models.Extension
|
|
for rows.Next() {
|
|
var ext models.Extension
|
|
var teamID, installedBy sql.NullString
|
|
if err := rows.Scan(
|
|
&ext.ID, &ext.ExtID, &ext.Name, &ext.Version, &ext.Tier,
|
|
&ext.Description, &ext.Author, &ext.Manifest, &ext.IsSystem,
|
|
&ext.IsEnabled, &ext.Scope, &teamID, &installedBy,
|
|
st(&ext.CreatedAt), st(&ext.UpdatedAt),
|
|
); err != nil {
|
|
return nil, err
|
|
}
|
|
ext.TeamID = NullableStringPtr(teamID)
|
|
ext.InstalledBy = NullableStringPtr(installedBy)
|
|
result = append(result, ext)
|
|
}
|
|
if result == nil {
|
|
result = make([]models.Extension, 0)
|
|
}
|
|
return result, rows.Err()
|
|
}
|