package sqlite import ( "context" "database/sql" "encoding/json" "time" "chat-switchboard/store" ) type PackageStore struct{} func NewPackageStore() *PackageStore { return &PackageStore{} } // ── Lifecycle ──────────────────────────────────── func (s *PackageStore) Seed(ctx context.Context, id, title, source string, manifest map[string]any) error { manifestJSON := ToJSON(manifest) _, err := DB.ExecContext(ctx, ` INSERT INTO packages (id, title, source, manifest, enabled, installed_at, updated_at) VALUES (?, ?, ?, ?, 1, datetime('now'), datetime('now')) ON CONFLICT (id) DO UPDATE SET title = excluded.title, manifest = excluded.manifest, source = excluded.source, updated_at = datetime('now')`, id, title, source, manifestJSON) return err } func (s *PackageStore) List(ctx context.Context) ([]store.PackageRegistration, error) { return s.scanMany(ctx, `SELECT `+pkgCols+` FROM packages p ORDER BY p.source, p.title`) } func (s *PackageStore) Get(ctx context.Context, id string) (*store.PackageRegistration, error) { return s.scanOne(ctx, `SELECT `+pkgCols+` FROM packages p WHERE p.id = ?`, id) } func (s *PackageStore) SetEnabled(ctx context.Context, id string, enabled bool) error { enabledInt := 0 if enabled { enabledInt = 1 } result, err := DB.ExecContext(ctx, `UPDATE packages SET enabled = ?, updated_at = datetime('now') WHERE id = ?`, enabledInt, id) if err != nil { return err } affected, _ := result.RowsAffected() if affected == 0 { return sql.ErrNoRows } return nil } func (s *PackageStore) SetStatus(ctx context.Context, id string, status string) error { result, err := DB.ExecContext(ctx, `UPDATE packages SET status = ?, updated_at = datetime('now') WHERE id = ?`, status, id) if err != nil { return err } affected, _ := result.RowsAffected() if affected == 0 { return sql.ErrNoRows } return nil } func (s *PackageStore) Delete(ctx context.Context, id string) error { result, err := DB.ExecContext(ctx, `DELETE FROM packages WHERE id = ? AND source != 'core'`, id) if err != nil { return err } affected, _ := result.RowsAffected() if affected == 0 { return sql.ErrNoRows } return nil } func (s *PackageStore) ListEnabled(ctx context.Context) ([]string, error) { rows, err := DB.QueryContext(ctx, `SELECT id FROM packages WHERE enabled = 1 ORDER BY title`) if err != nil { return nil, err } defer rows.Close() var ids []string for rows.Next() { var id string if err := rows.Scan(&id); err != nil { continue } ids = append(ids, id) } return ids, rows.Err() } // ── Extended lifecycle ─────────────────────────── func (s *PackageStore) Create(ctx context.Context, pkg *store.PackageRegistration) error { now := time.Now().UTC() manifestJSON := ToJSON(pkg.Manifest) if pkg.Status == "" { pkg.Status = "active" } _, err := DB.ExecContext(ctx, ` INSERT INTO packages (id, title, type, version, description, author, tier, is_system, scope, team_id, installed_by, manifest, enabled, status, schema_version, package_settings, source, installed_at, updated_at) VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)`, pkg.ID, pkg.Title, pkg.Type, pkg.Version, pkg.Description, pkg.Author, pkg.Tier, pkg.IsSystem, pkg.Scope, nullStrPtr(pkg.TeamID), nullStrPtr(pkg.InstalledBy), manifestJSON, pkg.Enabled, pkg.Status, pkg.SchemaVersion, defaultJSON(pkg.PackageSettings), pkg.Source, now.Format(timeFmt), now.Format(timeFmt), ) if err != nil { return err } pkg.InstalledAt = now.Format(timeFmt) pkg.UpdatedAt = now.Format(timeFmt) return nil } func (s *PackageStore) Update(ctx context.Context, id string, pkg *store.PackageRegistration) error { manifestJSON := ToJSON(pkg.Manifest) _, err := DB.ExecContext(ctx, ` UPDATE packages SET title = ?, type = ?, version = ?, description = ?, author = ?, tier = ?, manifest = ?, is_system = ?, enabled = ?, updated_at = datetime('now') WHERE id = ?`, pkg.Title, pkg.Type, pkg.Version, pkg.Description, pkg.Author, pkg.Tier, manifestJSON, pkg.IsSystem, pkg.Enabled, id, ) return err } func (s *PackageStore) ListByType(ctx context.Context, pkgType string) ([]store.PackageRegistration, error) { return s.scanMany(ctx, `SELECT `+pkgCols+` FROM packages p WHERE p.type = ? ORDER BY p.title`, pkgType) } func (s *PackageStore) ListEnabledByType(ctx context.Context, pkgType string) ([]store.PackageRegistration, error) { return s.scanMany(ctx, `SELECT `+pkgCols+` FROM packages p WHERE p.type = ? AND p.enabled = 1 ORDER BY p.title`, pkgType) } // ── Extension runtime ──────────────────────────── func (s *PackageStore) ListForUser(ctx context.Context, userID string) ([]store.UserPackage, error) { rows, err := DB.QueryContext(ctx, ` SELECT `+pkgCols+`, pus.is_enabled AS user_enabled, pus.settings AS user_settings FROM packages p LEFT JOIN package_user_settings pus ON pus.package_id = p.id AND pus.user_id = ? WHERE p.enabled = 1 AND p.type IN ('extension', 'full') AND (p.is_system = 1 OR COALESCE(pus.is_enabled, 1) = 1) AND ( p.scope = 'global' OR (p.scope = 'personal' AND p.installed_by = ?) OR (p.scope = 'team' AND p.team_id IN ( SELECT team_id FROM team_members WHERE user_id = ? )) ) ORDER BY p.title`, userID, userID, userID) if err != nil { return nil, err } defer rows.Close() var result []store.UserPackage for rows.Next() { var up store.UserPackage var teamID, installedBy sql.NullString var manifestJSON string var pkgSettings string var enabledInt int var isSystemInt int var userEnabled sql.NullBool var userSettings []byte if err := rows.Scan( &up.ID, &up.Title, &up.Type, &up.Version, &up.Description, &up.Author, &up.Tier, &isSystemInt, &up.Scope, &teamID, &installedBy, &manifestJSON, &enabledInt, &up.Status, &up.SchemaVersion, &pkgSettings, &up.Source, &up.InstalledAt, &up.UpdatedAt, &userEnabled, &userSettings, ); err != nil { return nil, err } up.IsSystem = isSystemInt != 0 up.Enabled = enabledInt != 0 up.TeamID = NullableStringPtr(teamID) up.InstalledBy = NullableStringPtr(installedBy) json.Unmarshal([]byte(manifestJSON), &up.Manifest) up.PackageSettings = json.RawMessage(pkgSettings) if userEnabled.Valid { up.UserEnabled = &userEnabled.Bool } if userSettings != nil { raw := json.RawMessage(userSettings) up.UserSettings = &raw } result = append(result, up) } if result == nil { result = make([]store.UserPackage, 0) } return result, rows.Err() } func (s *PackageStore) GetUserSettings(ctx context.Context, pkgID, userID string) (*store.PackageUserSettings, error) { var pus store.PackageUserSettings err := DB.QueryRowContext(ctx, ` SELECT package_id, user_id, settings, is_enabled FROM package_user_settings WHERE package_id = ? AND user_id = ?`, pkgID, userID, ).Scan(&pus.PackageID, &pus.UserID, &pus.Settings, &pus.IsEnabled) if err != nil { return nil, err } return &pus, nil } func (s *PackageStore) SetUserSettings(ctx context.Context, pus *store.PackageUserSettings) error { _, err := DB.ExecContext(ctx, ` INSERT INTO package_user_settings (package_id, user_id, settings, is_enabled) VALUES (?, ?, ?, ?) ON CONFLICT (package_id, user_id) DO UPDATE SET settings = EXCLUDED.settings, is_enabled = EXCLUDED.is_enabled`, pus.PackageID, pus.UserID, pus.Settings, pus.IsEnabled, ) return err } func (s *PackageStore) DeleteUserSettings(ctx context.Context, pkgID, userID string) error { _, err := DB.ExecContext(ctx, ` DELETE FROM package_user_settings WHERE package_id = ? AND user_id = ?`, pkgID, userID, ) return err } // ── Helpers ────────────────────────────────────── const pkgCols = `p.id, p.title, p.type, p.version, p.description, p.author, p.tier, p.is_system, p.scope, p.team_id, p.installed_by, p.manifest, p.enabled, p.status, p.schema_version, p.package_settings, p.source, p.installed_at, p.updated_at` func (s *PackageStore) scanOne(ctx context.Context, query string, args ...interface{}) (*store.PackageRegistration, error) { var pkg store.PackageRegistration var teamID, installedBy sql.NullString var manifestJSON string var pkgSettings string var enabledInt int var isSystemInt int err := DB.QueryRowContext(ctx, query, args...).Scan( &pkg.ID, &pkg.Title, &pkg.Type, &pkg.Version, &pkg.Description, &pkg.Author, &pkg.Tier, &isSystemInt, &pkg.Scope, &teamID, &installedBy, &manifestJSON, &enabledInt, &pkg.Status, &pkg.SchemaVersion, &pkgSettings, &pkg.Source, &pkg.InstalledAt, &pkg.UpdatedAt, ) if err == sql.ErrNoRows { return nil, nil } if err != nil { return nil, err } pkg.IsSystem = isSystemInt != 0 pkg.Enabled = enabledInt != 0 pkg.TeamID = NullableStringPtr(teamID) pkg.InstalledBy = NullableStringPtr(installedBy) json.Unmarshal([]byte(manifestJSON), &pkg.Manifest) pkg.PackageSettings = json.RawMessage(pkgSettings) return &pkg, nil } func (s *PackageStore) scanMany(ctx context.Context, query string, args ...interface{}) ([]store.PackageRegistration, error) { rows, err := DB.QueryContext(ctx, query, args...) if err != nil { return nil, err } defer rows.Close() var result []store.PackageRegistration for rows.Next() { var pkg store.PackageRegistration var teamID, installedBy sql.NullString var manifestJSON string var pkgSettings string var enabledInt int var isSystemInt int if err := rows.Scan( &pkg.ID, &pkg.Title, &pkg.Type, &pkg.Version, &pkg.Description, &pkg.Author, &pkg.Tier, &isSystemInt, &pkg.Scope, &teamID, &installedBy, &manifestJSON, &enabledInt, &pkg.Status, &pkg.SchemaVersion, &pkgSettings, &pkg.Source, &pkg.InstalledAt, &pkg.UpdatedAt, ); err != nil { return nil, err } pkg.IsSystem = isSystemInt != 0 pkg.Enabled = enabledInt != 0 pkg.TeamID = NullableStringPtr(teamID) pkg.InstalledBy = NullableStringPtr(installedBy) json.Unmarshal([]byte(manifestJSON), &pkg.Manifest) pkg.PackageSettings = json.RawMessage(pkgSettings) result = append(result, pkg) } return result, rows.Err() } // ── Scoped visibility (v0.30.0) ────────────────── func (s *PackageStore) ListVisiblePackages(ctx context.Context, userID string) ([]store.PackageRegistration, error) { return s.scanMany(ctx, ` SELECT `+pkgCols+` FROM packages p WHERE p.enabled = 1 AND ( p.scope = 'global' OR (p.scope = 'personal' AND p.installed_by = ?) OR (p.scope = 'team' AND p.team_id IN ( SELECT team_id FROM team_members WHERE user_id = ? )) ) ORDER BY p.source, p.title`, userID, userID) } // ── Package lifecycle (v0.30.0) ────────────────── func (s *PackageStore) SetSchemaVersion(ctx context.Context, id string, version int) error { result, err := DB.ExecContext(ctx, `UPDATE packages SET schema_version = ?, updated_at = datetime('now') WHERE id = ?`, version, id) if err != nil { return err } affected, _ := result.RowsAffected() if affected == 0 { return sql.ErrNoRows } return nil } func (s *PackageStore) GetPackageSettings(ctx context.Context, id string) (json.RawMessage, error) { var settings string err := DB.QueryRowContext(ctx, `SELECT package_settings FROM packages WHERE id = ?`, id).Scan(&settings) if err != nil { return nil, err } return json.RawMessage(settings), nil } func (s *PackageStore) SetPackageSettings(ctx context.Context, id string, settings json.RawMessage) error { result, err := DB.ExecContext(ctx, `UPDATE packages SET package_settings = ?, updated_at = datetime('now') WHERE id = ?`, string(settings), id) if err != nil { return err } affected, _ := result.RowsAffected() if affected == 0 { return sql.ErrNoRows } return nil } func nullStrPtr(s *string) sql.NullString { if s == nil { return sql.NullString{} } return sql.NullString{String: *s, Valid: true} } // defaultJSON returns the raw message or '{}' if nil/empty. func defaultJSON(raw json.RawMessage) string { if len(raw) == 0 { return "{}" } return string(raw) }