package models import ( "context" "encoding/json" "fmt" "time" "github.com/jackc/pgx/v5/pgxpool" ) type Campaign struct { ID string `json:"id"` RepositoryID string `json:"repository_id"` Name string `json:"name"` Type string `json:"type"` Status string `json:"status"` StartedAt time.Time `json:"started_at"` FinishedAt *time.Time `json:"finished_at,omitempty"` Tags json.RawMessage `json:"tags,omitempty"` Metadata json.RawMessage `json:"metadata,omitempty"` CreatedAt time.Time `json:"created_at"` // Joined fields. RepoName string `json:"repo_name,omitempty"` ArtifactCount int `json:"artifact_count,omitempty"` } type CreateCampaignParams struct { RepositoryID string Name string Type string Tags json.RawMessage Metadata json.RawMessage } func CreateCampaign(ctx context.Context, pool *pgxpool.Pool, p CreateCampaignParams) (*Campaign, error) { if p.Tags == nil { p.Tags = json.RawMessage("{}") } if p.Metadata == nil { p.Metadata = json.RawMessage("{}") } c := &Campaign{} err := pool.QueryRow(ctx, ` INSERT INTO campaigns (repository_id, name, type, tags, metadata) VALUES ($1, $2, $3, $4, $5) RETURNING id, repository_id, name, type, status, started_at, finished_at, tags, metadata, created_at `, p.RepositoryID, p.Name, p.Type, p.Tags, p.Metadata).Scan( &c.ID, &c.RepositoryID, &c.Name, &c.Type, &c.Status, &c.StartedAt, &c.FinishedAt, &c.Tags, &c.Metadata, &c.CreatedAt, ) if err != nil { return nil, fmt.Errorf("creating campaign: %w", err) } return c, nil } func FinishCampaign(ctx context.Context, pool *pgxpool.Pool, id string) error { _, err := pool.Exec(ctx, ` UPDATE campaigns SET status = 'finished', finished_at = NOW() WHERE id = $1 `, id) if err != nil { return fmt.Errorf("finishing campaign: %w", err) } return nil } func GetCampaign(ctx context.Context, pool *pgxpool.Pool, id string) (*Campaign, error) { c := &Campaign{} err := pool.QueryRow(ctx, ` SELECT c.id, c.repository_id, c.name, c.type, c.status, c.started_at, c.finished_at, c.tags, c.metadata, c.created_at, r.name, (SELECT COUNT(*) FROM artifacts a WHERE a.campaign_id = c.id) FROM campaigns c JOIN repositories r ON r.id = c.repository_id WHERE c.id = $1 `, id).Scan( &c.ID, &c.RepositoryID, &c.Name, &c.Type, &c.Status, &c.StartedAt, &c.FinishedAt, &c.Tags, &c.Metadata, &c.CreatedAt, &c.RepoName, &c.ArtifactCount, ) if err != nil { return nil, fmt.Errorf("getting campaign: %w", err) } return c, nil } func ListCampaigns(ctx context.Context, pool *pgxpool.Pool, repoID string, limit, offset int) ([]Campaign, int, error) { if limit <= 0 { limit = 50 } baseQuery := ` FROM campaigns c JOIN repositories r ON r.id = c.repository_id WHERE 1=1 ` args := []any{} argN := 1 if repoID != "" { baseQuery += fmt.Sprintf(" AND c.repository_id = $%d", argN) args = append(args, repoID) argN++ } var total int err := pool.QueryRow(ctx, "SELECT COUNT(*) "+baseQuery, args...).Scan(&total) if err != nil { return nil, 0, fmt.Errorf("counting campaigns: %w", err) } selectQuery := fmt.Sprintf(` SELECT c.id, c.repository_id, c.name, c.type, c.status, c.started_at, c.finished_at, c.tags, c.metadata, c.created_at, r.name, (SELECT COUNT(*) FROM artifacts a WHERE a.campaign_id = c.id) %s ORDER BY c.created_at DESC LIMIT $%d OFFSET $%d `, baseQuery, argN, argN+1) args = append(args, limit, offset) rows, err := pool.Query(ctx, selectQuery, args...) if err != nil { return nil, 0, fmt.Errorf("listing campaigns: %w", err) } defer rows.Close() var campaigns []Campaign for rows.Next() { var c Campaign if err := rows.Scan( &c.ID, &c.RepositoryID, &c.Name, &c.Type, &c.Status, &c.StartedAt, &c.FinishedAt, &c.Tags, &c.Metadata, &c.CreatedAt, &c.RepoName, &c.ArtifactCount, ); err != nil { return nil, 0, fmt.Errorf("scanning campaign: %w", err) } campaigns = append(campaigns, c) } return campaigns, total, nil }