201 lines
6.9 KiB
Go
201 lines
6.9 KiB
Go
package postgres
|
|
|
|
import (
|
|
"context"
|
|
"database/sql"
|
|
"encoding/json"
|
|
|
|
"git.gobha.me/xcaliber/chat-switchboard/models"
|
|
)
|
|
|
|
type ExtensionStore struct{}
|
|
|
|
func NewExtensionStore() *ExtensionStore { return &ExtensionStore{} }
|
|
|
|
func (s *ExtensionStore) Create(ctx context.Context, ext *models.Extension) error {
|
|
return DB.QueryRowContext(ctx, `
|
|
INSERT INTO extensions (ext_id, name, version, tier, description, author,
|
|
manifest, is_system, is_enabled, scope, team_id, installed_by)
|
|
VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9,$10,$11,$12)
|
|
RETURNING id, created_at, updated_at`,
|
|
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),
|
|
).Scan(&ext.ID, &ext.CreatedAt, &ext.UpdatedAt)
|
|
}
|
|
|
|
func (s *ExtensionStore) GetByID(ctx context.Context, id string) (*models.Extension, error) {
|
|
return s.scanOne(ctx, `SELECT * FROM extensions WHERE id = $1`, id)
|
|
}
|
|
|
|
func (s *ExtensionStore) GetByExtID(ctx context.Context, extID string) (*models.Extension, error) {
|
|
return s.scanOne(ctx, `SELECT * FROM extensions WHERE ext_id = $1`, extID)
|
|
}
|
|
|
|
func (s *ExtensionStore) Update(ctx context.Context, id string, ext *models.Extension) error {
|
|
_, err := DB.ExecContext(ctx, `
|
|
UPDATE extensions SET
|
|
name = $2, version = $3, description = $4, author = $5,
|
|
manifest = $6, is_system = $7, is_enabled = $8,
|
|
updated_at = now()
|
|
WHERE id = $1`,
|
|
id, ext.Name, ext.Version, ext.Description, ext.Author,
|
|
ext.Manifest, ext.IsSystem, ext.IsEnabled,
|
|
)
|
|
return err
|
|
}
|
|
|
|
func (s *ExtensionStore) Delete(ctx context.Context, id string) error {
|
|
_, err := DB.ExecContext(ctx, `DELETE FROM extensions WHERE id = $1`, id)
|
|
return err
|
|
}
|
|
|
|
func (s *ExtensionStore) ListAll(ctx context.Context) ([]models.Extension, error) {
|
|
return s.scanMany(ctx, `SELECT * FROM extensions ORDER BY name`)
|
|
}
|
|
|
|
// ListEnabled returns all globally enabled extensions regardless of user.
|
|
// TODO(v0.29.0): Used by Starlark runtime to load server-side extensions at startup.
|
|
func (s *ExtensionStore) ListEnabled(ctx context.Context) ([]models.Extension, error) {
|
|
return s.scanMany(ctx, `SELECT * FROM extensions WHERE is_enabled = true 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 = $1
|
|
WHERE e.is_enabled = true
|
|
AND (e.is_system = true OR COALESCE(eus.is_enabled, true) = true)
|
|
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,
|
|
&ue.CreatedAt, &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()
|
|
}
|
|
|
|
// GetUserSettings returns per-user settings for a specific extension.
|
|
// TODO(v0.29.0): Used by Starlark runtime to load per-user config for server-side extensions.
|
|
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 = $1 AND user_id = $2`,
|
|
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 ($1, $2, $3, $4)
|
|
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
|
|
}
|
|
|
|
// DeleteUserSettings removes per-user settings, reverting to defaults.
|
|
// TODO(v0.29.0): Exposed via user settings UI when Starlark extensions have per-user config.
|
|
func (s *ExtensionStore) DeleteUserSettings(ctx context.Context, extID, userID string) error {
|
|
_, err := DB.ExecContext(ctx, `
|
|
DELETE FROM extension_user_settings WHERE extension_id = $1 AND user_id = $2`,
|
|
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,
|
|
&ext.CreatedAt, &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,
|
|
&ext.CreatedAt, &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()
|
|
}
|