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, } }