package models import ( "context" "fmt" "time" cairnapi "github.com/mattnite/cairn/internal/api" "gorm.io/gorm" ) func GetOrCreateSignature(ctx context.Context, db *gorm.DB, repoID uint, fingerprint string, sampleTrace *string) (*cairnapi.CrashSignature, bool, error) { sig := &CrashSignature{} err := db.WithContext(ctx).Where("repository_id = ? AND fingerprint = ?", repoID, fingerprint).First(sig).Error if err == nil { sig.LastSeenAt = time.Now() sig.OccurrenceCount++ if sig.SampleTrace == nil && sampleTrace != nil { sig.SampleTrace = sampleTrace } if saveErr := db.WithContext(ctx).Save(sig).Error; saveErr != nil { return nil, false, fmt.Errorf("updating signature: %w", saveErr) } out := crashSignatureFromModel(*sig) return &out, false, nil } if err != gorm.ErrRecordNotFound { return nil, false, fmt.Errorf("querying signature: %w", err) } now := time.Now() sig = &CrashSignature{ RepositoryID: repoID, Fingerprint: fingerprint, SampleTrace: sampleTrace, FirstSeenAt: now, LastSeenAt: now, OccurrenceCount: 1, } if err := db.WithContext(ctx).Create(sig).Error; err != nil { return nil, false, fmt.Errorf("creating signature: %w", err) } out := crashSignatureFromModel(*sig) return &out, true, nil } func CreateCrashGroup(ctx context.Context, db *gorm.DB, sigID, repoID uint, title string) (*cairnapi.CrashGroup, error) { now := time.Now() group := &CrashGroup{ CrashSignatureID: sigID, RepositoryID: repoID, Title: title, Status: "open", FirstSeenAt: now, LastSeenAt: now, } if err := db.WithContext(ctx).Create(group).Error; err != nil { return nil, fmt.Errorf("creating crash group: %w", err) } return enrichCrashGroup(ctx, db, *group) } func ListCrashGroups(ctx context.Context, db *gorm.DB, repoID *uint, status string, limit, offset int) ([]cairnapi.CrashGroup, int64, error) { if limit <= 0 { limit = 50 } query := db.WithContext(ctx).Model(&CrashGroup{}) if repoID != nil { query = query.Where("repository_id = ?", *repoID) } if status != "" { query = query.Where("status = ?", status) } var total int64 if err := query.Count(&total).Error; err != nil { return nil, 0, fmt.Errorf("counting crash groups: %w", err) } var dbGroups []CrashGroup if err := query.Order("last_seen_at DESC").Limit(limit).Offset(offset).Find(&dbGroups).Error; err != nil { return nil, 0, fmt.Errorf("listing crash groups: %w", err) } groups := make([]cairnapi.CrashGroup, 0, len(dbGroups)) for _, m := range dbGroups { g, err := enrichCrashGroup(ctx, db, m) if err != nil { return nil, 0, err } groups = append(groups, *g) } return groups, total, nil } func GetCrashGroup(ctx context.Context, db *gorm.DB, id uint) (*cairnapi.CrashGroup, error) { group := &CrashGroup{} if err := db.WithContext(ctx).First(group, id).Error; err != nil { return nil, fmt.Errorf("getting crash group: %w", err) } return enrichCrashGroup(ctx, db, *group) } func enrichCrashGroup(ctx context.Context, db *gorm.DB, model CrashGroup) (*cairnapi.CrashGroup, error) { repo := &Repository{} if err := db.WithContext(ctx).First(repo, model.RepositoryID).Error; err != nil { return nil, fmt.Errorf("loading crash group repository: %w", err) } sig := &CrashSignature{} if err := db.WithContext(ctx).First(sig, model.CrashSignatureID).Error; err != nil { return nil, fmt.Errorf("loading crash signature: %w", err) } group := crashGroupFromModel(model) group.RepoName = repo.Name group.Fingerprint = sig.Fingerprint group.OccurrenceCount = sig.OccurrenceCount group.SampleTrace = sig.SampleTrace return &group, nil } func crashSignatureFromModel(m CrashSignature) cairnapi.CrashSignature { return cairnapi.CrashSignature{ ID: m.ID, RepositoryID: m.RepositoryID, Fingerprint: m.Fingerprint, SampleTrace: m.SampleTrace, FirstSeenAt: m.FirstSeenAt, LastSeenAt: m.LastSeenAt, OccurrenceCount: m.OccurrenceCount, } } func crashGroupFromModel(m CrashGroup) cairnapi.CrashGroup { return cairnapi.CrashGroup{ ID: m.ID, RepositoryID: m.RepositoryID, CrashSignatureID: m.CrashSignatureID, Title: m.Title, Status: m.Status, ForgejoIssueID: m.ForgejoIssueID, FirstSeenAt: m.FirstSeenAt, LastSeenAt: m.LastSeenAt, CreatedAt: m.CreatedAt, UpdatedAt: m.UpdatedAt, } }