feat: implement database and storage abstractions

- Added `internal/db/store.go` to define the Store interface for database operations, including user, organization, app, channel, release, patch, and artifact management.
- Introduced `internal/models/models.go` changes to reflect updated organization and artifact response structures.
- Created `internal/storage/factory.go`, `local.go`, and `s3.go` to implement storage backends for local filesystem and S3-compatible services.
- Removed deprecated `internal/storage/storage.go` and refactored storage interface into `internal/storage/store.go`.
- Updated frontend JavaScript in `web/js/app.js` to display server health information, including storage and database backend details.
This commit is contained in:
Tony
2026-06-12 08:45:01 +08:00
parent 246260a8f5
commit 3bb4ab494e
31 changed files with 1698 additions and 509 deletions
+87 -200
View File
@@ -3,7 +3,6 @@ package db
import (
"context"
"fmt"
"time"
"github.com/google/uuid"
"github.com/jackc/pgx/v5"
@@ -11,13 +10,13 @@ import (
"github.com/jackc/pgx/v5/pgxpool"
)
// DB wraps the PostgreSQL connection pool with query methods.
type DB struct {
// PgStore implements Store backed by PostgreSQL.
type PgStore struct {
pool *pgxpool.Pool
}
// New creates a new database connection pool.
func New(ctx context.Context, databaseURL string) (*DB, error) {
// NewPostgresStore creates a new PostgreSQL-backed Store.
func NewPostgresStore(ctx context.Context, databaseURL string) (*PgStore, error) {
config, err := pgxpool.ParseConfig(databaseURL)
if err != nil {
return nil, fmt.Errorf("failed to parse database URL: %w", err)
@@ -32,40 +31,37 @@ func New(ctx context.Context, databaseURL string) (*DB, error) {
return nil, fmt.Errorf("failed to ping database: %w", err)
}
return &DB{pool: pool}, nil
return &PgStore{pool: pool}, nil
}
// Close closes the database connection pool.
func (db *DB) Close() {
func (db *PgStore) Close() {
db.pool.Close()
}
// Pool returns the underlying connection pool.
func (db *DB) Pool() *pgxpool.Pool {
return db.pool
}
// Exec is a convenience wrapper for pool.Exec.
func (db *DB) Exec(ctx context.Context, sql string, args ...interface{}) (pgconn.CommandTag, error) {
func (db *PgStore) exec(ctx context.Context, sql string, args ...interface{}) (pgconn.CommandTag, error) {
return db.pool.Exec(ctx, sql, args...)
}
// QueryRow is a convenience wrapper for pool.QueryRow.
func (db *DB) QueryRow(ctx context.Context, sql string, args ...interface{}) pgx.Row {
func (db *PgStore) queryRow(ctx context.Context, sql string, args ...interface{}) pgx.Row {
return db.pool.QueryRow(ctx, sql, args...)
}
// Query is a convenience wrapper for pool.Query.
func (db *DB) Query(ctx context.Context, sql string, args ...interface{}) (pgx.Rows, error) {
func (db *PgStore) query(ctx context.Context, sql string, args ...interface{}) (pgx.Rows, error) {
return db.pool.Query(ctx, sql, args...)
}
// --- User queries ---
// CreateUser creates a new user and returns the ID.
func (db *DB) CreateUser(ctx context.Context, email, name, passwordHash string) (int, error) {
func (db *PgStore) CreateUser(ctx context.Context, email, name, passwordHash string) (int, error) {
var id int
err := db.QueryRow(ctx,
err := db.queryRow(ctx,
`INSERT INTO users (email, name, password_hash) VALUES ($1, $2, $3) RETURNING id`,
email, name, passwordHash,
).Scan(&id)
@@ -73,9 +69,9 @@ func (db *DB) CreateUser(ctx context.Context, email, name, passwordHash string)
}
// GetUserByEmail retrieves a user by email.
func (db *DB) GetUserByEmail(ctx context.Context, email string) (*UserRow, error) {
func (db *PgStore) GetUserByEmail(ctx context.Context, email string) (*UserRow, error) {
row := &UserRow{}
err := db.QueryRow(ctx,
err := db.queryRow(ctx,
`SELECT id, email, name, password_hash, created_at FROM users WHERE email = $1`,
email,
).Scan(&row.ID, &row.Email, &row.Name, &row.PasswordHash, &row.CreatedAt)
@@ -86,9 +82,9 @@ func (db *DB) GetUserByEmail(ctx context.Context, email string) (*UserRow, error
}
// GetUserByID retrieves a user by ID.
func (db *DB) GetUserByID(ctx context.Context, id int) (*UserRow, error) {
func (db *PgStore) GetUserByID(ctx context.Context, id int) (*UserRow, error) {
row := &UserRow{}
err := db.QueryRow(ctx,
err := db.queryRow(ctx,
`SELECT id, email, name, password_hash, created_at FROM users WHERE id = $1`,
id,
).Scan(&row.ID, &row.Email, &row.Name, &row.PasswordHash, &row.CreatedAt)
@@ -98,21 +94,12 @@ func (db *DB) GetUserByID(ctx context.Context, id int) (*UserRow, error) {
return row, nil
}
// UserRow represents a user row from the database.
type UserRow struct {
ID int
Email string
Name string
PasswordHash string
CreatedAt time.Time
}
// --- Organization queries ---
// CreateOrganization creates a new organization.
func (db *DB) CreateOrganization(ctx context.Context, name, orgType string) (int, error) {
func (db *PgStore) CreateOrganization(ctx context.Context, name, orgType string) (int, error) {
var id int
err := db.QueryRow(ctx,
err := db.queryRow(ctx,
`INSERT INTO organizations (name, type) VALUES ($1, $2) RETURNING id`,
name, orgType,
).Scan(&id)
@@ -120,8 +107,8 @@ func (db *DB) CreateOrganization(ctx context.Context, name, orgType string) (int
}
// GetOrganizationsByUserID returns all organizations a user belongs to.
func (db *DB) GetOrganizationsByUserID(ctx context.Context, userID int) ([]OrgMembershipRow, error) {
rows, err := db.Query(ctx,
func (db *PgStore) GetOrganizationsByUserID(ctx context.Context, userID int) ([]OrgMembershipRow, error) {
rows, err := db.query(ctx,
`SELECT o.id, o.name, o.type, om.role
FROM organizations o
JOIN organization_memberships om ON o.id = om.organization_id
@@ -143,17 +130,10 @@ func (db *DB) GetOrganizationsByUserID(ctx context.Context, userID int) ([]OrgMe
return result, nil
}
// OrgMembershipRow represents an organization membership query result.
type OrgMembershipRow struct {
OrgID int
OrgName string
OrgType string
Role string
}
// AddUserToOrganization adds a user to an organization.
func (db *DB) AddUserToOrganization(ctx context.Context, userID, orgID int, role string) error {
_, err := db.Exec(ctx,
func (db *PgStore) AddUserToOrganization(ctx context.Context, userID, orgID int, role string) error {
_, err := db.exec(ctx,
`INSERT INTO organization_memberships (user_id, organization_id, role)
VALUES ($1, $2, $3) ON CONFLICT DO NOTHING`,
userID, orgID, role)
@@ -163,9 +143,9 @@ func (db *DB) AddUserToOrganization(ctx context.Context, userID, orgID int, role
// --- App queries ---
// CreateApp creates a new app and returns it.
func (db *DB) CreateApp(ctx context.Context, orgID int, displayName string) (*AppRow, error) {
func (db *PgStore) CreateApp(ctx context.Context, orgID int, displayName string) (*AppRow, error) {
row := &AppRow{}
err := db.QueryRow(ctx,
err := db.queryRow(ctx,
`INSERT INTO apps (organization_id, display_name) VALUES ($1, $2)
RETURNING id, organization_id, display_name, created_at, updated_at`,
orgID, displayName,
@@ -177,8 +157,8 @@ func (db *DB) CreateApp(ctx context.Context, orgID int, displayName string) (*Ap
}
// GetAppsByOrganization returns all apps for an organization.
func (db *DB) GetAppsByOrganization(ctx context.Context, orgID int) ([]AppRow, error) {
rows, err := db.Query(ctx,
func (db *PgStore) GetAppsByOrganization(ctx context.Context, orgID int) ([]AppRow, error) {
rows, err := db.query(ctx,
`SELECT id, organization_id, display_name, created_at, updated_at
FROM apps WHERE organization_id = $1 ORDER BY display_name`, orgID)
if err != nil {
@@ -198,9 +178,9 @@ func (db *DB) GetAppsByOrganization(ctx context.Context, orgID int) ([]AppRow, e
}
// GetAppByID retrieves a single app by ID.
func (db *DB) GetAppByID(ctx context.Context, appID uuid.UUID) (*AppRow, error) {
func (db *PgStore) GetAppByID(ctx context.Context, appID uuid.UUID) (*AppRow, error) {
row := &AppRow{}
err := db.QueryRow(ctx,
err := db.queryRow(ctx,
`SELECT id, organization_id, display_name, created_at, updated_at FROM apps WHERE id = $1`,
appID,
).Scan(&row.ID, &row.OrganizationID, &row.DisplayName, &row.CreatedAt, &row.UpdatedAt)
@@ -211,7 +191,7 @@ func (db *DB) GetAppByID(ctx context.Context, appID uuid.UUID) (*AppRow, error)
}
// GetAppByIDString retrieves a single app by its string ID.
func (db *DB) GetAppByIDString(ctx context.Context, appID string) (*AppRow, error) {
func (db *PgStore) GetAppByIDString(ctx context.Context, appID string) (*AppRow, error) {
id, err := uuid.Parse(appID)
if err != nil {
return nil, fmt.Errorf("invalid app_id: %w", err)
@@ -220,14 +200,14 @@ func (db *DB) GetAppByIDString(ctx context.Context, appID string) (*AppRow, erro
}
// DeleteApp deletes an app by ID.
func (db *DB) DeleteApp(ctx context.Context, appID uuid.UUID) error {
_, err := db.Exec(ctx, `DELETE FROM apps WHERE id = $1`, appID)
func (db *PgStore) DeleteApp(ctx context.Context, appID uuid.UUID) error {
_, err := db.exec(ctx, `DELETE FROM apps WHERE id = $1`, appID)
return err
}
// GetAppsByUserID returns all apps the user has access to via org memberships.
func (db *DB) GetAppsByUserID(ctx context.Context, userID int) ([]AppRow, error) {
rows, err := db.Query(ctx,
func (db *PgStore) GetAppsByUserID(ctx context.Context, userID int) ([]AppRow, error) {
rows, err := db.query(ctx,
`SELECT DISTINCT a.id, a.organization_id, a.display_name, a.created_at, a.updated_at
FROM apps a
JOIN organization_memberships om ON a.organization_id = om.organization_id
@@ -249,21 +229,13 @@ func (db *DB) GetAppsByUserID(ctx context.Context, userID int) ([]AppRow, error)
return result, nil
}
// AppRow represents an app row from the database.
type AppRow struct {
ID uuid.UUID
OrganizationID int
DisplayName string
CreatedAt time.Time
UpdatedAt time.Time
}
// --- Channel queries ---
// CreateChannel creates a new channel for an app.
func (db *DB) CreateChannel(ctx context.Context, appID uuid.UUID, name string) (*ChannelRow, error) {
func (db *PgStore) CreateChannel(ctx context.Context, appID uuid.UUID, name string) (*ChannelRow, error) {
row := &ChannelRow{}
err := db.QueryRow(ctx,
err := db.queryRow(ctx,
`INSERT INTO channels (app_id, name) VALUES ($1, $2)
RETURNING id, app_id, name, created_at`,
appID, name,
@@ -275,8 +247,8 @@ func (db *DB) CreateChannel(ctx context.Context, appID uuid.UUID, name string) (
}
// GetChannelsByAppID returns all channels for an app.
func (db *DB) GetChannelsByAppID(ctx context.Context, appID uuid.UUID) ([]ChannelRow, error) {
rows, err := db.Query(ctx,
func (db *PgStore) GetChannelsByAppID(ctx context.Context, appID uuid.UUID) ([]ChannelRow, error) {
rows, err := db.query(ctx,
`SELECT id, app_id, name, created_at FROM channels WHERE app_id = $1 ORDER BY name`, appID)
if err != nil {
return nil, err
@@ -295,9 +267,9 @@ func (db *DB) GetChannelsByAppID(ctx context.Context, appID uuid.UUID) ([]Channe
}
// GetChannelByAppIDAndName retrieves a specific channel.
func (db *DB) GetChannelByAppIDAndName(ctx context.Context, appID uuid.UUID, name string) (*ChannelRow, error) {
func (db *PgStore) GetChannelByAppIDAndName(ctx context.Context, appID uuid.UUID, name string) (*ChannelRow, error) {
row := &ChannelRow{}
err := db.QueryRow(ctx,
err := db.queryRow(ctx,
`SELECT id, app_id, name, created_at FROM channels WHERE app_id = $1 AND name = $2`,
appID, name,
).Scan(&row.ID, &row.AppID, &row.Name, &row.CreatedAt)
@@ -307,20 +279,13 @@ func (db *DB) GetChannelByAppIDAndName(ctx context.Context, appID uuid.UUID, nam
return row, nil
}
// ChannelRow represents a channel row from the database.
type ChannelRow struct {
ID int
AppID uuid.UUID
Name string
CreatedAt time.Time
}
// --- Release queries ---
// CreateRelease creates a new release.
func (db *DB) CreateRelease(ctx context.Context, appID uuid.UUID, version, flutterRevision string, flutterVersion, displayName *string) (*ReleaseRow, error) {
func (db *PgStore) CreateRelease(ctx context.Context, appID uuid.UUID, version, flutterRevision string, flutterVersion, displayName *string) (*ReleaseRow, error) {
row := &ReleaseRow{}
err := db.QueryRow(ctx,
err := db.queryRow(ctx,
`INSERT INTO releases (app_id, version, flutter_revision, flutter_version, display_name)
VALUES ($1, $2, $3, $4, $5)
RETURNING id, app_id, version, flutter_revision, flutter_version, display_name, notes, created_at, updated_at`,
@@ -333,9 +298,9 @@ func (db *DB) CreateRelease(ctx context.Context, appID uuid.UUID, version, flutt
}
// GetReleaseByID retrieves a release by ID.
func (db *DB) GetReleaseByID(ctx context.Context, releaseID int) (*ReleaseRow, error) {
func (db *PgStore) GetReleaseByID(ctx context.Context, releaseID int) (*ReleaseRow, error) {
row := &ReleaseRow{}
err := db.QueryRow(ctx,
err := db.queryRow(ctx,
`SELECT id, app_id, version, flutter_revision, flutter_version, display_name, notes, created_at, updated_at
FROM releases WHERE id = $1`, releaseID,
).Scan(&row.ID, &row.AppID, &row.Version, &row.FlutterRevision, &row.FlutterVersion, &row.DisplayName, &row.Notes, &row.CreatedAt, &row.UpdatedAt)
@@ -346,9 +311,9 @@ func (db *DB) GetReleaseByID(ctx context.Context, releaseID int) (*ReleaseRow, e
}
// GetReleaseByAppIDAndVersion retrieves a release by app and version.
func (db *DB) GetReleaseByAppIDAndVersion(ctx context.Context, appID uuid.UUID, version string) (*ReleaseRow, error) {
func (db *PgStore) GetReleaseByAppIDAndVersion(ctx context.Context, appID uuid.UUID, version string) (*ReleaseRow, error) {
row := &ReleaseRow{}
err := db.QueryRow(ctx,
err := db.queryRow(ctx,
`SELECT id, app_id, version, flutter_revision, flutter_version, display_name, notes, created_at, updated_at
FROM releases WHERE app_id = $1 AND version = $2`, appID, version,
).Scan(&row.ID, &row.AppID, &row.Version, &row.FlutterRevision, &row.FlutterVersion, &row.DisplayName, &row.Notes, &row.CreatedAt, &row.UpdatedAt)
@@ -359,13 +324,13 @@ func (db *DB) GetReleaseByAppIDAndVersion(ctx context.Context, appID uuid.UUID,
}
// GetReleasesByAppID returns all releases for an app.
func (db *DB) GetReleasesByAppID(ctx context.Context, appID uuid.UUID, sideloadableOnly bool) ([]ReleaseRow, error) {
func (db *PgStore) GetReleasesByAppID(ctx context.Context, appID uuid.UUID, sideloadableOnly bool) ([]ReleaseRow, error) {
query := `SELECT id, app_id, version, flutter_revision, flutter_version, display_name, notes, created_at, updated_at
FROM releases WHERE app_id = $1 ORDER BY created_at DESC`
// Note: sideloadable filtering is done via artifact join if needed
_ = sideloadableOnly
rows, err := db.Query(ctx, query, appID)
rows, err := db.query(ctx, query, appID)
if err != nil {
return nil, err
}
@@ -383,8 +348,8 @@ func (db *DB) GetReleasesByAppID(ctx context.Context, appID uuid.UUID, sideloada
}
// UpdateReleasePlatformStatus upserts a platform status for a release.
func (db *DB) UpdateReleasePlatformStatus(ctx context.Context, releaseID int, platform, status string, metadata map[string]interface{}) error {
_, err := db.Exec(ctx,
func (db *PgStore) UpdateReleasePlatformStatus(ctx context.Context, releaseID int, platform, status string, metadata map[string]interface{}) error {
_, err := db.exec(ctx,
`INSERT INTO release_platform_statuses (release_id, platform, status, metadata)
VALUES ($1, $2, $3, $4)
ON CONFLICT (release_id, platform)
@@ -394,8 +359,8 @@ func (db *DB) UpdateReleasePlatformStatus(ctx context.Context, releaseID int, pl
}
// GetReleasePlatformStatuses returns all platform statuses for a release.
func (db *DB) GetReleasePlatformStatuses(ctx context.Context, releaseID int) (map[string]string, error) {
rows, err := db.Query(ctx,
func (db *PgStore) GetReleasePlatformStatuses(ctx context.Context, releaseID int) (map[string]string, error) {
rows, err := db.query(ctx,
`SELECT platform, status FROM release_platform_statuses WHERE release_id = $1`, releaseID)
if err != nil {
return nil, err
@@ -413,25 +378,13 @@ func (db *DB) GetReleasePlatformStatuses(ctx context.Context, releaseID int) (ma
return result, nil
}
// ReleaseRow represents a release row from the database.
type ReleaseRow struct {
ID int
AppID uuid.UUID
Version string
FlutterRevision string
FlutterVersion *string
DisplayName *string
Notes *string
CreatedAt time.Time
UpdatedAt time.Time
}
// --- Release artifact queries ---
// CreateReleaseArtifact creates a release artifact record.
func (db *DB) CreateReleaseArtifact(ctx context.Context, releaseID int, arch, platform, hash, storageKey string, size int64, canSideload bool, podfileLockHash *string) (*ArtifactRow, error) {
func (db *PgStore) CreateReleaseArtifact(ctx context.Context, releaseID int, arch, platform, hash, storageKey string, size int64, canSideload bool, podfileLockHash *string) (*ArtifactRow, error) {
row := &ArtifactRow{}
err := db.QueryRow(ctx,
err := db.queryRow(ctx,
`INSERT INTO release_artifacts (release_id, arch, platform, hash, size, storage_key, can_sideload, podfile_lock_hash)
VALUES ($1, $2, $3, $4, $5, $6, $7, $8)
RETURNING id, release_id, arch, platform, hash, size, storage_key, can_sideload, podfile_lock_hash, created_at`,
@@ -444,7 +397,7 @@ func (db *DB) CreateReleaseArtifact(ctx context.Context, releaseID int, arch, pl
}
// GetReleaseArtifacts returns artifacts for a release, optionally filtered.
func (db *DB) GetReleaseArtifacts(ctx context.Context, releaseID int, arch, platform *string) ([]ArtifactRow, error) {
func (db *PgStore) GetReleaseArtifacts(ctx context.Context, releaseID int, arch, platform *string) ([]ArtifactRow, error) {
query := `SELECT id, release_id, arch, platform, hash, size, storage_key, can_sideload, podfile_lock_hash, created_at
FROM release_artifacts WHERE release_id = $1`
args := []interface{}{releaseID}
@@ -460,7 +413,7 @@ func (db *DB) GetReleaseArtifacts(ctx context.Context, releaseID int, arch, plat
query += " ORDER BY arch"
rows, err := db.Query(ctx, query, args...)
rows, err := db.query(ctx, query, args...)
if err != nil {
return nil, err
}
@@ -477,35 +430,22 @@ func (db *DB) GetReleaseArtifacts(ctx context.Context, releaseID int, arch, plat
return result, nil
}
// ArtifactRow represents a release artifact row.
type ArtifactRow struct {
ID int
ReleaseID int
Arch string
Platform string
Hash string
Size int64
StorageKey string
CanSideload bool
PodfileLockHash *string
CreatedAt time.Time
}
// --- Patch queries ---
// GetNextPatchNumber returns the next patch number for a release.
func (db *DB) GetNextPatchNumber(ctx context.Context, releaseID int) (int, error) {
func (db *PgStore) GetNextPatchNumber(ctx context.Context, releaseID int) (int, error) {
var num int
err := db.QueryRow(ctx,
err := db.queryRow(ctx,
`SELECT COALESCE(MAX(number), 0) + 1 FROM patches WHERE release_id = $1`, releaseID,
).Scan(&num)
return num, err
}
// CreatePatch creates a new patch.
func (db *DB) CreatePatch(ctx context.Context, releaseID int, number int, notes *string) (*PatchRow, error) {
func (db *PgStore) CreatePatch(ctx context.Context, releaseID int, number int, notes *string) (*PatchRow, error) {
row := &PatchRow{}
err := db.QueryRow(ctx,
err := db.queryRow(ctx,
`INSERT INTO patches (release_id, number, notes) VALUES ($1, $2, $3)
RETURNING id, release_id, number, notes, created_at`,
releaseID, number, notes,
@@ -517,9 +457,9 @@ func (db *DB) CreatePatch(ctx context.Context, releaseID int, number int, notes
}
// GetPatchByID retrieves a patch by ID.
func (db *DB) GetPatchByID(ctx context.Context, patchID int) (*PatchRow, error) {
func (db *PgStore) GetPatchByID(ctx context.Context, patchID int) (*PatchRow, error) {
row := &PatchRow{}
err := db.QueryRow(ctx,
err := db.queryRow(ctx,
`SELECT id, release_id, number, notes, created_at FROM patches WHERE id = $1`, patchID,
).Scan(&row.ID, &row.ReleaseID, &row.Number, &row.Notes, &row.CreatedAt)
if err != nil {
@@ -529,9 +469,9 @@ func (db *DB) GetPatchByID(ctx context.Context, patchID int) (*PatchRow, error)
}
// GetLatestPatchForRelease returns the latest patch for a release (by number).
func (db *DB) GetLatestPatchForRelease(ctx context.Context, releaseID int) (*PatchRow, error) {
func (db *PgStore) GetLatestPatchForRelease(ctx context.Context, releaseID int) (*PatchRow, error) {
row := &PatchRow{}
err := db.QueryRow(ctx,
err := db.queryRow(ctx,
`SELECT id, release_id, number, notes, created_at FROM patches
WHERE release_id = $1 ORDER BY number DESC LIMIT 1`, releaseID,
).Scan(&row.ID, &row.ReleaseID, &row.Number, &row.Notes, &row.CreatedAt)
@@ -542,8 +482,8 @@ func (db *DB) GetLatestPatchForRelease(ctx context.Context, releaseID int) (*Pat
}
// GetPatchesByReleaseID returns all patches for a release.
func (db *DB) GetPatchesByReleaseID(ctx context.Context, releaseID int) ([]PatchWithChannelRow, error) {
rows, err := db.Query(ctx,
func (db *PgStore) GetPatchesByReleaseID(ctx context.Context, releaseID int) ([]PatchWithChannelRow, error) {
rows, err := db.query(ctx,
`SELECT p.id, p.release_id, p.number, p.notes, p.created_at,
COALESCE(pc.channel_id, 0) as channel_id, COALESCE(pc.promoted_at, p.created_at) as promoted_at
FROM patches p
@@ -567,17 +507,17 @@ func (db *DB) GetPatchesByReleaseID(ctx context.Context, releaseID int) ([]Patch
}
// PromotePatch promotes a patch to a channel.
func (db *DB) PromotePatch(ctx context.Context, patchID, channelID int) error {
_, err := db.Exec(ctx,
func (db *PgStore) PromotePatch(ctx context.Context, patchID, channelID int) error {
_, err := db.exec(ctx,
`INSERT INTO patch_channels (patch_id, channel_id) VALUES ($1, $2) ON CONFLICT DO NOTHING`,
patchID, channelID)
return err
}
// GetLatestPromotedPatch returns the latest patch promoted to a specific channel for a release.
func (db *DB) GetLatestPromotedPatch(ctx context.Context, releaseID, channelID int) (*PatchWithArtifactRow, error) {
func (db *PgStore) GetLatestPromotedPatch(ctx context.Context, releaseID, channelID int) (*PatchWithArtifactRow, error) {
row := &PatchWithArtifactRow{}
err := db.QueryRow(ctx,
err := db.queryRow(ctx,
`SELECT p.id, p.release_id, p.number, p.notes, p.created_at,
pa.hash, pa.storage_key, pa.hash_signature
FROM patches p
@@ -594,44 +534,12 @@ func (db *DB) GetLatestPromotedPatch(ctx context.Context, releaseID, channelID i
return row, nil
}
// PatchRow represents a patch row.
type PatchRow struct {
ID int
ReleaseID int
Number int
Notes *string
CreatedAt time.Time
}
// PatchWithChannelRow includes channel promotion info.
type PatchWithChannelRow struct {
ID int
ReleaseID int
Number int
Notes *string
CreatedAt time.Time
ChannelID int
PromotedAt time.Time
}
// PatchWithArtifactRow includes artifact info for patch check.
type PatchWithArtifactRow struct {
ID int
ReleaseID int
Number int
Notes *string
CreatedAt time.Time
Hash string
StorageKey string
HashSignature *string
}
// --- Patch artifact queries ---
// CreatePatchArtifact creates a patch artifact record.
func (db *DB) CreatePatchArtifact(ctx context.Context, patchID int, arch, platform, hash, storageKey string, size int64, hashSignature, podfileLockHash *string) (*PatchArtifactRow, error) {
func (db *PgStore) CreatePatchArtifact(ctx context.Context, patchID int, arch, platform, hash, storageKey string, size int64, hashSignature, podfileLockHash *string) (*PatchArtifactRow, error) {
row := &PatchArtifactRow{}
err := db.QueryRow(ctx,
err := db.queryRow(ctx,
`INSERT INTO patch_artifacts (patch_id, arch, platform, hash, size, storage_key, hash_signature, podfile_lock_hash)
VALUES ($1, $2, $3, $4, $5, $6, $7, $8)
RETURNING id, patch_id, arch, platform, hash, size, storage_key, hash_signature, podfile_lock_hash, created_at`,
@@ -643,25 +551,12 @@ func (db *DB) CreatePatchArtifact(ctx context.Context, patchID int, arch, platfo
return row, nil
}
// PatchArtifactRow represents a patch artifact row.
type PatchArtifactRow struct {
ID int
PatchID int
Arch string
Platform string
Hash string
Size int64
StorageKey string
HashSignature *string
PodfileLockHash *string
CreatedAt time.Time
}
// --- Patch events ---
// InsertPatchEvent records a patch lifecycle event.
func (db *DB) InsertPatchEvent(ctx context.Context, appID uuid.UUID, clientID, arch, platform, releaseVersion, eventType string, patchNumber int, timestamp int64, message *string) error {
_, err := db.Exec(ctx,
func (db *PgStore) InsertPatchEvent(ctx context.Context, appID uuid.UUID, clientID, arch, platform, releaseVersion, eventType string, patchNumber int, timestamp int64, message *string) error {
_, err := db.exec(ctx,
`INSERT INTO patch_events (app_id, client_id, arch, patch_number, platform, release_version, event_type, timestamp, message)
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9)`,
appID, clientID, arch, patchNumber, platform, releaseVersion, eventType, timestamp, message)
@@ -671,16 +566,16 @@ func (db *DB) InsertPatchEvent(ctx context.Context, appID uuid.UUID, clientID, a
// --- Rolled back patches ---
// RollbackPatch marks a patch as rolled back.
func (db *DB) RollbackPatch(ctx context.Context, releaseID, patchNumber int) error {
_, err := db.Exec(ctx,
func (db *PgStore) RollbackPatch(ctx context.Context, releaseID, patchNumber int) error {
_, err := db.exec(ctx,
`INSERT INTO rolled_back_patches (release_id, patch_number) VALUES ($1, $2) ON CONFLICT DO NOTHING`,
releaseID, patchNumber)
return err
}
// GetRolledBackPatchNumbers returns all rolled-back patch numbers for a release.
func (db *DB) GetRolledBackPatchNumbers(ctx context.Context, releaseID int) ([]int, error) {
rows, err := db.Query(ctx,
func (db *PgStore) GetRolledBackPatchNumbers(ctx context.Context, releaseID int) ([]int, error) {
rows, err := db.query(ctx,
`SELECT patch_number FROM rolled_back_patches WHERE release_id = $1`, releaseID)
if err != nil {
return nil, err
@@ -701,17 +596,17 @@ func (db *DB) GetRolledBackPatchNumbers(ctx context.Context, releaseID int) ([]i
// --- Targeted device patching ---
// AddPatchTargetDevice adds a device to a patch's target list.
func (db *DB) AddPatchTargetDevice(ctx context.Context, patchID int, clientID string) error {
_, err := db.Exec(ctx,
func (db *PgStore) AddPatchTargetDevice(ctx context.Context, patchID int, clientID string) error {
_, err := db.exec(ctx,
`INSERT INTO patch_target_devices (patch_id, client_id) VALUES ($1, $2) ON CONFLICT DO NOTHING`,
patchID, clientID)
return err
}
// IsPatchTargetedToDevice checks if a patch is targeted to a specific device.
func (db *DB) IsPatchTargetedToDevice(ctx context.Context, patchID int, clientID string) (bool, error) {
func (db *PgStore) IsPatchTargetedToDevice(ctx context.Context, patchID int, clientID string) (bool, error) {
var count int
err := db.QueryRow(ctx,
err := db.queryRow(ctx,
`SELECT COUNT(*) FROM patch_target_devices WHERE patch_id = $1 AND client_id = $2`,
patchID, clientID,
).Scan(&count)
@@ -719,8 +614,8 @@ func (db *DB) IsPatchTargetedToDevice(ctx context.Context, patchID int, clientID
}
// GetPatchTargetDevices returns all targeted devices for a patch.
func (db *DB) GetPatchTargetDevices(ctx context.Context, patchID int) ([]string, error) {
rows, err := db.Query(ctx,
func (db *PgStore) GetPatchTargetDevices(ctx context.Context, patchID int) ([]string, error) {
rows, err := db.query(ctx,
`SELECT client_id FROM patch_target_devices WHERE patch_id = $1`, patchID)
if err != nil {
return nil, err
@@ -739,9 +634,9 @@ func (db *DB) GetPatchTargetDevices(ctx context.Context, patchID int) ([]string,
}
// HasTargetDevices checks if a patch has any targeted device restrictions.
func (db *DB) HasTargetDevices(ctx context.Context, patchID int) (bool, error) {
func (db *PgStore) HasTargetDevices(ctx context.Context, patchID int) (bool, error) {
var count int
err := db.QueryRow(ctx,
err := db.queryRow(ctx,
`SELECT COUNT(*) FROM patch_target_devices WHERE patch_id = $1`, patchID,
).Scan(&count)
return count > 0, err
@@ -750,8 +645,8 @@ func (db *DB) HasTargetDevices(ctx context.Context, patchID int) (bool, error) {
// --- Admin queries ---
// ListAllUsers returns all registered users (admin endpoint).
func (db *DB) ListAllUsers(ctx context.Context) ([]UserListRow, error) {
rows, err := db.Query(ctx,
func (db *PgStore) ListAllUsers(ctx context.Context) ([]UserListRow, error) {
rows, err := db.query(ctx,
`SELECT u.id, u.email, u.name, u.created_at,
COALESCE(string_agg(DISTINCT om.role, ', '), '') as roles
FROM users u
@@ -774,11 +669,3 @@ func (db *DB) ListAllUsers(ctx context.Context) ([]UserListRow, error) {
return result, nil
}
// UserListRow represents a user with role info for admin listing.
type UserListRow struct {
ID int
Email string
Name string
CreatedAt time.Time
Role string
}