// ABOUTME: SQLite storage layer for metrics receiver using GORM. // ABOUTME: Handles database initialization and metric storage/retrieval. package receiver import ( "encoding/json" "fmt" "time" "github.com/glebarez/sqlite" "gorm.io/gorm" "gorm.io/gorm/logger" ) // Metric represents a stored metric record in the database type Metric struct { ID uint `gorm:"primaryKey"` Organization string `gorm:"index:idx_org_repo;not null"` Repository string `gorm:"index:idx_org_repo;not null"` Workflow string `gorm:"not null"` Job string `gorm:"not null"` RunID string `gorm:"index;not null"` ReceivedAt time.Time `gorm:"index;not null"` Payload string `gorm:"type:text;not null"` // JSON-encoded RunSummary } // MetricResponse is the API response type with Payload as embedded JSON object type MetricResponse struct { ID uint `json:"id"` Organization string `json:"organization"` Repository string `json:"repository"` Workflow string `json:"workflow"` Job string `json:"job"` RunID string `json:"run_id"` ReceivedAt time.Time `json:"received_at"` Payload json.RawMessage `json:"payload"` } // ToResponse converts a Metric to a MetricResponse with Payload as JSON object func (m *Metric) ToResponse() MetricResponse { return MetricResponse{ ID: m.ID, Organization: m.Organization, Repository: m.Repository, Workflow: m.Workflow, Job: m.Job, RunID: m.RunID, ReceivedAt: m.ReceivedAt, Payload: json.RawMessage(m.Payload), } } // Store handles SQLite storage for metrics using GORM type Store struct { db *gorm.DB } // NewStore creates a new SQLite store at the given path func NewStore(dbPath string) (*Store, error) { db, err := gorm.Open(sqlite.Open(dbPath), &gorm.Config{ Logger: logger.Default.LogMode(logger.Silent), }) if err != nil { return nil, fmt.Errorf("opening database: %w", err) } if err := db.AutoMigrate(&Metric{}); err != nil { return nil, fmt.Errorf("migrating schema: %w", err) } return &Store{db: db}, nil } // SaveMetric stores a metrics payload in the database func (s *Store) SaveMetric(payload *MetricsPayload) (uint, error) { summaryJSON, err := json.Marshal(payload.Summary) if err != nil { return 0, fmt.Errorf("marshaling summary: %w", err) } metric := Metric{ Organization: payload.Execution.Organization, Repository: payload.Execution.Repository, Workflow: payload.Execution.Workflow, Job: payload.Execution.Job, RunID: payload.Execution.RunID, ReceivedAt: time.Now().UTC(), Payload: string(summaryJSON), } result := s.db.Create(&metric) if result.Error != nil { return 0, fmt.Errorf("inserting metric: %w", result.Error) } return metric.ID, nil } // GetMetricsByWorkflowJob retrieves all metrics for a specific workflow and job func (s *Store) GetMetricsByWorkflowJob(org, repo, workflow, job string) ([]Metric, error) { var metrics []Metric result := s.db.Where("organization = ? AND repository = ? AND workflow = ? AND job = ?", org, repo, workflow, job).Order("received_at DESC").Find(&metrics) return metrics, result.Error } // Close closes the database connection func (s *Store) Close() error { sqlDB, err := s.db.DB() if err != nil { return err } return sqlDB.Close() }