This repository has been archived on 2026-04-03. You can view files and clone it. You cannot open issues or pull requests or push a commit.
Files
core/server/store/sqlite/packages.go
Jeffrey Smith 6b9ce92103
All checks were successful
CI/CD / detect-changes (push) Successful in 3s
CI/CD / test-frontend (push) Has been skipped
CI/CD / test-runners (push) Has been skipped
CI/CD / e2e-smoke (push) Has been skipped
CI/CD / test-go-pg (push) Successful in 2m55s
CI/CD / test-sqlite (push) Successful in 3m7s
CI/CD / build-and-deploy (push) Successful in 1m19s
Feat v0.9.4 package adoption roles (#78)
Co-authored-by: Jeffrey Smith <jasafpro@gmail.com>
Co-committed-by: Jeffrey Smith <jasafpro@gmail.com>
2026-04-03 16:23:43 +00:00

469 lines
14 KiB
Go

package sqlite
import (
"context"
"database/sql"
"encoding/json"
"time"
"armature/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"
}
adoptableInt := 0
if pkg.Adoptable {
adoptableInt = 1
}
_, 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, adoptable, adopted_from,
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, adoptableInt, nullStrPtr(pkg.AdoptedFrom),
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, adoptedFrom sql.NullString
var manifestJSON string
var pkgSettings string
var enabledInt int
var isSystemInt int
var adoptableInt 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, &adoptableInt, &adoptedFrom,
&up.InstalledAt, &up.UpdatedAt,
&userEnabled, &userSettings,
); err != nil {
return nil, err
}
up.IsSystem = isSystemInt != 0
up.Enabled = enabledInt != 0
up.Adoptable = adoptableInt != 0
up.TeamID = NullableStringPtr(teamID)
up.InstalledBy = NullableStringPtr(installedBy)
up.AdoptedFrom = NullableStringPtr(adoptedFrom)
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.adoptable, p.adopted_from,
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, adoptedFrom sql.NullString
var manifestJSON string
var pkgSettings string
var enabledInt int
var isSystemInt int
var adoptableInt 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, &adoptableInt, &adoptedFrom,
&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.Adoptable = adoptableInt != 0
pkg.TeamID = NullableStringPtr(teamID)
pkg.InstalledBy = NullableStringPtr(installedBy)
pkg.AdoptedFrom = NullableStringPtr(adoptedFrom)
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, adoptedFrom sql.NullString
var manifestJSON string
var pkgSettings string
var enabledInt int
var isSystemInt int
var adoptableInt 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, &adoptableInt, &adoptedFrom,
&pkg.InstalledAt, &pkg.UpdatedAt,
); err != nil {
return nil, err
}
pkg.IsSystem = isSystemInt != 0
pkg.Enabled = enabledInt != 0
pkg.Adoptable = adoptableInt != 0
pkg.TeamID = NullableStringPtr(teamID)
pkg.InstalledBy = NullableStringPtr(installedBy)
pkg.AdoptedFrom = NullableStringPtr(adoptedFrom)
json.Unmarshal([]byte(manifestJSON), &pkg.Manifest)
pkg.PackageSettings = json.RawMessage(pkgSettings)
result = append(result, pkg)
}
return result, rows.Err()
}
// ── Scoped visibility ──────────────────
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 ──────────────────
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
}
// ── Team-level settings ─────────────────
func (s *PackageStore) GetTeamSettings(ctx context.Context, pkgID, teamID string) (json.RawMessage, error) {
var settings string
err := DB.QueryRowContext(ctx,
`SELECT settings FROM package_team_settings WHERE package_id = ? AND team_id = ?`,
pkgID, teamID).Scan(&settings)
if err != nil {
return nil, err
}
return json.RawMessage(settings), nil
}
func (s *PackageStore) SetTeamSettings(ctx context.Context, pkgID, teamID string, settings json.RawMessage) error {
_, err := DB.ExecContext(ctx, `
INSERT INTO package_team_settings (package_id, team_id, settings, updated_at)
VALUES (?, ?, ?, datetime('now'))
ON CONFLICT (package_id, team_id)
DO UPDATE SET settings = EXCLUDED.settings, updated_at = datetime('now')`,
pkgID, teamID, string(settings))
return err
}
func (s *PackageStore) DeleteTeamSettings(ctx context.Context, pkgID, teamID string) error {
_, err := DB.ExecContext(ctx,
`DELETE FROM package_team_settings WHERE package_id = ? AND team_id = ?`,
pkgID, teamID)
return err
}
// ── v0.9.4 — Package Adoption ───────────────
func (s *PackageStore) ListAdoptable(ctx context.Context) ([]store.PackageRegistration, error) {
return s.scanMany(ctx, `
SELECT `+pkgCols+`
FROM packages p
WHERE p.adoptable = 1 AND p.scope = 'global' AND p.enabled = 1
ORDER BY p.title`)
}
func (s *PackageStore) GetByAdoptedFrom(ctx context.Context, sourceID, teamID string) (*store.PackageRegistration, error) {
return s.scanOne(ctx, `
SELECT `+pkgCols+`
FROM packages p
WHERE p.adopted_from = ? AND p.team_id = ?`, sourceID, teamID)
}
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)
}