cairn/internal/models/target.go

300 lines
7.7 KiB
Go

package models
import (
"context"
"encoding/json"
"fmt"
"time"
cairnapi "github.com/mattnite/cairn/internal/api"
"gorm.io/gorm"
)
type CreateTargetParams struct {
RepositoryID uint
Name string
Type string
Tags json.RawMessage
Metadata json.RawMessage
}
func GetOrCreateTarget(ctx context.Context, db *gorm.DB, p CreateTargetParams) (*cairnapi.Target, error) {
if p.Tags == nil {
p.Tags = json.RawMessage("{}")
}
if p.Metadata == nil {
p.Metadata = json.RawMessage("{}")
}
target := &Target{}
err := db.WithContext(ctx).
Where("repository_id = ? AND name = ?", p.RepositoryID, p.Name).
First(target).Error
if err == gorm.ErrRecordNotFound {
target = &Target{
RepositoryID: p.RepositoryID,
Name: p.Name,
Type: p.Type,
Tags: p.Tags,
Metadata: p.Metadata,
}
if err := db.WithContext(ctx).Create(target).Error; err != nil {
return nil, fmt.Errorf("creating target: %w", err)
}
} else if err != nil {
return nil, fmt.Errorf("querying target: %w", err)
}
return enrichTarget(ctx, db, *target)
}
func GetTarget(ctx context.Context, db *gorm.DB, id uint) (*cairnapi.Target, error) {
target := &Target{}
if err := db.WithContext(ctx).First(target, id).Error; err != nil {
return nil, fmt.Errorf("getting target: %w", err)
}
return enrichTarget(ctx, db, *target)
}
func ListTargets(ctx context.Context, db *gorm.DB, repoID *uint, limit, offset int) ([]cairnapi.Target, int64, error) {
if limit <= 0 {
limit = 50
}
query := db.WithContext(ctx).Model(&Target{})
if repoID != nil {
query = query.Where("repository_id = ?", *repoID)
}
var total int64
if err := query.Count(&total).Error; err != nil {
return nil, 0, fmt.Errorf("counting targets: %w", err)
}
var dbTargets []Target
if err := query.Order("updated_at DESC").Limit(limit).Offset(offset).Find(&dbTargets).Error; err != nil {
return nil, 0, fmt.Errorf("listing targets: %w", err)
}
targets := make([]cairnapi.Target, 0, len(dbTargets))
for _, m := range dbTargets {
t, err := enrichTarget(ctx, db, m)
if err != nil {
return nil, 0, err
}
targets = append(targets, *t)
}
return targets, total, nil
}
func enrichTarget(ctx context.Context, db *gorm.DB, model Target) (*cairnapi.Target, error) {
repo := &Repository{}
if err := db.WithContext(ctx).First(repo, model.RepositoryID).Error; err != nil {
return nil, fmt.Errorf("loading target repository: %w", err)
}
var runCount int64
if err := db.WithContext(ctx).Model(&Run{}).Where("target_id = ?", model.ID).Count(&runCount).Error; err != nil {
return nil, fmt.Errorf("counting target runs: %w", err)
}
var corpusCount int64
if err := db.WithContext(ctx).Model(&CorpusEntry{}).Where("target_id = ?", model.ID).Count(&corpusCount).Error; err != nil {
return nil, fmt.Errorf("counting target corpus: %w", err)
}
t := targetFromModel(model)
t.RepoName = repo.Name
t.RunCount = runCount
t.CorpusCount = corpusCount
return &t, nil
}
func targetFromModel(m Target) cairnapi.Target {
return cairnapi.Target{
ID: m.ID,
RepositoryID: m.RepositoryID,
Name: m.Name,
Type: m.Type,
Tags: m.Tags,
Metadata: m.Metadata,
CreatedAt: m.CreatedAt,
UpdatedAt: m.UpdatedAt,
}
}
// Run functions
func CreateRun(ctx context.Context, db *gorm.DB, targetID, commitID uint) (*cairnapi.Run, error) {
run := &Run{
TargetID: targetID,
CommitID: commitID,
Status: "running",
StartedAt: time.Now(),
Tags: json.RawMessage("{}"),
Metadata: json.RawMessage("{}"),
}
if err := db.WithContext(ctx).Create(run).Error; err != nil {
return nil, fmt.Errorf("creating run: %w", err)
}
return enrichRun(ctx, db, *run)
}
func FinishRun(ctx context.Context, db *gorm.DB, id uint) error {
now := time.Now()
if err := db.WithContext(ctx).Model(&Run{}).Where("id = ?", id).Updates(map[string]any{
"status": "finished",
"finished_at": now,
}).Error; err != nil {
return fmt.Errorf("finishing run: %w", err)
}
return nil
}
func GetRun(ctx context.Context, db *gorm.DB, id uint) (*cairnapi.Run, error) {
run := &Run{}
if err := db.WithContext(ctx).First(run, id).Error; err != nil {
return nil, fmt.Errorf("getting run: %w", err)
}
return enrichRun(ctx, db, *run)
}
func ListRuns(ctx context.Context, db *gorm.DB, targetID *uint, limit, offset int) ([]cairnapi.Run, int64, error) {
if limit <= 0 {
limit = 50
}
query := db.WithContext(ctx).Model(&Run{})
if targetID != nil {
query = query.Where("target_id = ?", *targetID)
}
var total int64
if err := query.Count(&total).Error; err != nil {
return nil, 0, fmt.Errorf("counting runs: %w", err)
}
var dbRuns []Run
if err := query.Order("created_at DESC").Limit(limit).Offset(offset).Find(&dbRuns).Error; err != nil {
return nil, 0, fmt.Errorf("listing runs: %w", err)
}
runs := make([]cairnapi.Run, 0, len(dbRuns))
for _, m := range dbRuns {
r, err := enrichRun(ctx, db, m)
if err != nil {
return nil, 0, err
}
runs = append(runs, *r)
}
return runs, total, nil
}
func enrichRun(ctx context.Context, db *gorm.DB, model Run) (*cairnapi.Run, error) {
target := &Target{}
if err := db.WithContext(ctx).First(target, model.TargetID).Error; err != nil {
return nil, fmt.Errorf("loading run target: %w", err)
}
repo := &Repository{}
if err := db.WithContext(ctx).First(repo, target.RepositoryID).Error; err != nil {
return nil, fmt.Errorf("loading run repository: %w", err)
}
commit := &Commit{}
if err := db.WithContext(ctx).First(commit, model.CommitID).Error; err != nil {
return nil, fmt.Errorf("loading run commit: %w", err)
}
var artifactCount int64
_ = db.WithContext(ctx).Model(&Artifact{}).Where("run_id = ?", model.ID).Count(&artifactCount).Error
r := runFromModel(model)
r.TargetName = target.Name
r.RepoName = repo.Name
r.CommitSHA = commit.SHA
r.ArtifactCount = artifactCount
return &r, nil
}
func runFromModel(m Run) cairnapi.Run {
return cairnapi.Run{
ID: m.ID,
TargetID: m.TargetID,
CommitID: m.CommitID,
Status: m.Status,
StartedAt: m.StartedAt,
FinishedAt: m.FinishedAt,
Tags: m.Tags,
Metadata: m.Metadata,
CreatedAt: m.CreatedAt,
}
}
// Corpus functions
type CreateCorpusEntryParams struct {
TargetID uint
RunID *uint
BlobKey string
BlobSize int64
Fingerprint *string
}
func CreateCorpusEntry(ctx context.Context, db *gorm.DB, p CreateCorpusEntryParams) (*cairnapi.CorpusEntry, error) {
entry := &CorpusEntry{
TargetID: p.TargetID,
RunID: p.RunID,
BlobKey: p.BlobKey,
BlobSize: p.BlobSize,
Fingerprint: p.Fingerprint,
}
if err := db.WithContext(ctx).Create(entry).Error; err != nil {
return nil, fmt.Errorf("creating corpus entry: %w", err)
}
return corpusEntryToAPI(*entry), nil
}
func ListCorpusEntries(ctx context.Context, db *gorm.DB, targetID uint, limit, offset int) ([]cairnapi.CorpusEntry, int64, error) {
if limit <= 0 {
limit = 1000
}
query := db.WithContext(ctx).Model(&CorpusEntry{}).Where("target_id = ?", targetID)
var total int64
if err := query.Count(&total).Error; err != nil {
return nil, 0, fmt.Errorf("counting corpus entries: %w", err)
}
var dbEntries []CorpusEntry
if err := query.Order("created_at DESC").Limit(limit).Offset(offset).Find(&dbEntries).Error; err != nil {
return nil, 0, fmt.Errorf("listing corpus entries: %w", err)
}
entries := make([]cairnapi.CorpusEntry, 0, len(dbEntries))
for _, e := range dbEntries {
entries = append(entries, *corpusEntryToAPI(e))
}
return entries, total, nil
}
func corpusEntryToAPI(m CorpusEntry) *cairnapi.CorpusEntry {
return &cairnapi.CorpusEntry{
ID: m.ID,
TargetID: m.TargetID,
RunID: m.RunID,
BlobKey: m.BlobKey,
BlobSize: m.BlobSize,
Fingerprint: m.Fingerprint,
CreatedAt: m.CreatedAt,
}
}