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`) } 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() } 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 } 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() }