Files
DoGaMa-serv/internal/persistence/sqlite/catalog.go
T

113 lines
4.7 KiB
Go

package sqlite
import (
"context"
"database/sql"
"encoding/json"
"errors"
"fmt"
"time"
"git.zaynet.fr/DoGaMa/DoGaMa-serv/internal/catalog"
"git.zaynet.fr/DoGaMa/DoGaMa-serv/internal/instance"
)
// Repository persists catalog snapshots and the desired instance registry.
type Repository struct {
db *sql.DB
now func() time.Time
}
func NewRepository(db *sql.DB) *Repository {
return &Repository{db: db, now: time.Now}
}
func (r *Repository) Sync(ctx context.Context, snapshots []catalog.Snapshot) error {
tx, err := r.db.BeginTx(ctx, nil)
if err != nil {
return fmt.Errorf("begin catalog sync: %w", err)
}
defer func() { _ = tx.Rollback() }()
now := r.now().UTC().Format(time.RFC3339Nano)
for _, snapshot := range snapshots {
var digest string
err := tx.QueryRowContext(ctx, "SELECT digest FROM template_versions WHERE template_id = ? AND version = ?", snapshot.Template.ID, snapshot.Template.Version).Scan(&digest)
if err == nil && digest != snapshot.Digest {
return fmt.Errorf("%w: %s@%s", catalog.ErrImmutableSnapshot, snapshot.Template.ID, snapshot.Template.Version)
}
if err != nil && !errors.Is(err, sql.ErrNoRows) {
return fmt.Errorf("check template snapshot: %w", err)
}
_, err = tx.ExecContext(ctx, `INSERT INTO templates(id, origin, trust_status, active_version, created_at, updated_at)
VALUES (?, ?, ?, ?, ?, ?)
ON CONFLICT(id) DO UPDATE SET active_version=excluded.active_version, updated_at=excluded.updated_at`,
snapshot.Template.ID, snapshot.Origin, snapshot.Origin, snapshot.Template.Version, now, now)
if err != nil {
return fmt.Errorf("upsert catalog template: %w", err)
}
if digest == "" {
_, err = tx.ExecContext(ctx, `INSERT INTO template_versions(template_id, version, schema_version, canonical_yaml, digest, game_id, game_name, description, created_at)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`, snapshot.Template.ID, snapshot.Template.Version, snapshot.Template.SchemaVersion, snapshot.CanonicalYAML, snapshot.Digest, snapshot.Template.Game.ID, snapshot.Template.Game.Name, snapshot.Template.Game.Description, now)
if err != nil {
return fmt.Errorf("insert template snapshot: %w", err)
}
}
}
if err := tx.Commit(); err != nil {
return fmt.Errorf("commit catalog sync: %w", err)
}
return nil
}
func (r *Repository) List(ctx context.Context) ([]catalog.Summary, error) {
rows, err := r.db.QueryContext(ctx, `SELECT t.id, t.active_version, v.game_id, v.game_name, v.description, t.trust_status, v.digest
FROM templates t JOIN template_versions v ON v.template_id=t.id AND v.version=t.active_version ORDER BY v.game_name, t.id`)
if err != nil {
return nil, fmt.Errorf("list catalog: %w", err)
}
defer rows.Close()
var result []catalog.Summary
for rows.Next() {
var summary catalog.Summary
if err := rows.Scan(&summary.ID, &summary.Version, &summary.GameID, &summary.GameName, &summary.Description, &summary.TrustStatus, &summary.Digest); err != nil {
return nil, fmt.Errorf("scan catalog: %w", err)
}
result = append(result, summary)
}
return result, rows.Err()
}
func (r *Repository) Get(ctx context.Context, id, version string) (catalog.Snapshot, error) {
var canonical, digest, origin string
err := r.db.QueryRowContext(ctx, `SELECT v.canonical_yaml, v.digest, t.origin FROM template_versions v JOIN templates t ON t.id=v.template_id
WHERE v.template_id=? AND v.version=?`, id, version).Scan(&canonical, &digest, &origin)
if errors.Is(err, sql.ErrNoRows) {
return catalog.Snapshot{}, catalog.ErrTemplateNotFound
}
if err != nil {
return catalog.Snapshot{}, fmt.Errorf("load template snapshot: %w", err)
}
var template catalog.Template
if err := json.Unmarshal([]byte(canonical), &template); err != nil {
return catalog.Snapshot{}, fmt.Errorf("decode stored template snapshot: %w", err)
}
return catalog.Snapshot{Template: template, CanonicalYAML: canonical, Digest: digest, Origin: origin}, nil
}
func (r *Repository) CreateDraft(ctx context.Context, draft instance.Draft) error {
if draft.ID == "" {
return errors.New("draft instance ID is required")
}
now := r.now().UTC().Format(time.RFC3339Nano)
previewJSON, err := json.Marshal(draft.Preview)
if err != nil {
return fmt.Errorf("encode draft preview: %w", err)
}
_, err = r.db.ExecContext(ctx, `INSERT INTO instances(id, slug, display_name, template_id, template_version, template_digest, revision, lifecycle_state, preview_json, plan_digest, created_at, updated_at)
VALUES (?, ?, ?, ?, ?, ?, 1, 'draft', ?, ?, ?, ?)`, draft.ID, draft.Preview.Slug, draft.Preview.DisplayName, draft.Preview.Template.ID, draft.Preview.Template.Version, draft.Preview.Template.Digest, string(previewJSON), draft.Preview.PlanDigest, now, now)
if err != nil {
return fmt.Errorf("create draft instance: %w", err)
}
return nil
}