345 lines
8.8 KiB
Go
345 lines
8.8 KiB
Go
package storage
|
|
|
|
import (
|
|
"context"
|
|
"database/sql"
|
|
"encoding/json"
|
|
"fmt"
|
|
"slices"
|
|
|
|
"github.com/LazyBachelor/LazyPM/internal/models"
|
|
|
|
"github.com/steveyegge/beads"
|
|
)
|
|
|
|
type BeadsService struct {
|
|
beads.Storage
|
|
}
|
|
|
|
func NewBeadsIssueStorage(ctx context.Context, storage beads.Storage, prefix string) (*BeadsService, error) {
|
|
issue_prefix, err := storage.GetConfig(ctx, "issue_prefix")
|
|
if err != nil || issue_prefix == "" {
|
|
if err := storage.SetConfig(ctx, "issue_prefix", prefix); err != nil {
|
|
return nil, fmt.Errorf("failed to set issue_prefix: %w", err)
|
|
}
|
|
}
|
|
|
|
storage.UnderlyingDB().Exec(`
|
|
CREATE TABLE IF NOT EXISTS sprints (
|
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
name TEXT,
|
|
issues TEXT,
|
|
sprint_num INTEGER UNIQUE,
|
|
is_backlog BOOLEAN DEFAULT 0
|
|
);
|
|
`)
|
|
|
|
backlogNum, err := getBacklogSprintNum(storage)
|
|
if err != nil {
|
|
_, err = storage.UnderlyingDB().Exec(
|
|
"INSERT INTO sprints (name, issues, sprint_num, is_backlog) VALUES (?, ?, 0, 1)",
|
|
"backlog", "[]",
|
|
)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to create backlog sprint: %w", err)
|
|
}
|
|
storage.SetConfig(ctx, "backlog_sprint", "0")
|
|
} else {
|
|
storage.SetConfig(ctx, "backlog_sprint", fmt.Sprintf("%d", backlogNum))
|
|
}
|
|
|
|
return &BeadsService{
|
|
Storage: storage,
|
|
}, nil
|
|
}
|
|
|
|
func (s *BeadsService) CreateIssue(ctx context.Context, issue *models.Issue, actor string) error {
|
|
if err := s.Storage.CreateIssue(ctx, issue, actor); err != nil {
|
|
return err
|
|
}
|
|
|
|
backlogNum, err := s.GetBacklogSprint(ctx)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
if err := s.AddIssueToSprint(ctx, issue.ID, backlogNum); err != nil {
|
|
return err
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (s *BeadsService) CreateIssues(ctx context.Context, issues []*models.Issue, actor string) error {
|
|
if err := s.Storage.CreateIssues(ctx, issues, actor); err != nil {
|
|
return err
|
|
}
|
|
|
|
backlogNum, err := s.GetBacklogSprint(ctx)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
for _, issue := range issues {
|
|
if err := s.AddIssueToSprint(ctx, issue.ID, backlogNum); err != nil {
|
|
continue
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (s *BeadsService) AllIssues(ctx context.Context) ([]models.Issue, error) {
|
|
issuesPtr, err := s.Storage.SearchIssues(ctx, "", models.IssueFilter{})
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
if len(issuesPtr) == 0 {
|
|
return []models.Issue{}, nil
|
|
}
|
|
|
|
issues := models.IssuesPtrToIssues(issuesPtr)
|
|
|
|
return issues, nil
|
|
}
|
|
|
|
func (s *BeadsService) DeleteIssues() error {
|
|
_, err := s.UnderlyingDB().Exec("DELETE FROM issues; DELETE FROM sprints;")
|
|
if err != nil {
|
|
return err
|
|
}
|
|
_, err = s.UnderlyingDB().Exec("INSERT INTO sprints (name, issues, sprint_num, is_backlog) VALUES ('backlog', '[]', 0, 1)")
|
|
return err
|
|
}
|
|
|
|
func (s *BeadsService) AddSprint(ctx context.Context) (int, error) {
|
|
var addSprint = "INSERT INTO sprints (sprint_num, issues) VALUES ((SELECT IFNULL(MAX(sprint_num), 0) + 1 FROM sprints), '[]');"
|
|
|
|
r, err := s.UnderlyingDB().Exec(addSprint)
|
|
if err != nil {
|
|
return 0, fmt.Errorf("failed to add sprint: %w", err)
|
|
}
|
|
|
|
id, err := r.LastInsertId()
|
|
if err != nil {
|
|
return 0, fmt.Errorf("failed to get last insert id: %w", err)
|
|
}
|
|
|
|
var sprintNum int
|
|
err = s.UnderlyingDB().QueryRow("SELECT sprint_num FROM sprints WHERE id = ?", id).Scan(&sprintNum)
|
|
if err != nil {
|
|
return 0, fmt.Errorf("failed to get sprint_num: %w", err)
|
|
}
|
|
|
|
return sprintNum, nil
|
|
}
|
|
|
|
func (s *BeadsService) RemoveSprint(ctx context.Context, sprintNum int) error {
|
|
result, err := s.UnderlyingDB().Exec("DELETE FROM sprints WHERE sprint_num = ?", sprintNum)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to remove sprint: %w", err)
|
|
}
|
|
|
|
rowsAffected, err := result.RowsAffected()
|
|
if err != nil {
|
|
return fmt.Errorf("failed to check rows affected: %w", err)
|
|
}
|
|
|
|
if rowsAffected == 0 {
|
|
return fmt.Errorf("sprint %d not found", sprintNum)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (s *BeadsService) GetSprints(ctx context.Context) ([]int, error) {
|
|
rows, err := s.UnderlyingDB().Query("SELECT sprint_num FROM sprints WHERE is_backlog = 0 ORDER BY sprint_num")
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to get sprints: %w", err)
|
|
}
|
|
defer rows.Close()
|
|
|
|
var sprints []int
|
|
for rows.Next() {
|
|
var sprintNum int
|
|
if err := rows.Scan(&sprintNum); err != nil {
|
|
return nil, fmt.Errorf("failed to scan sprint: %w", err)
|
|
}
|
|
sprints = append(sprints, sprintNum)
|
|
}
|
|
|
|
if err := rows.Err(); err != nil {
|
|
return nil, fmt.Errorf("error iterating sprints: %w", err)
|
|
}
|
|
|
|
return sprints, nil
|
|
}
|
|
|
|
func (s *BeadsService) GetIssuesBySprint(ctx context.Context, sprintNum int) ([]*models.Issue, error) {
|
|
var issuesJSON string
|
|
err := s.UnderlyingDB().QueryRow("SELECT issues FROM sprints WHERE sprint_num = ?", sprintNum).Scan(&issuesJSON)
|
|
if err != nil {
|
|
if err == sql.ErrNoRows {
|
|
return []*models.Issue{}, nil
|
|
}
|
|
return nil, fmt.Errorf("failed to get sprint issues: %w", err)
|
|
}
|
|
|
|
var issueIDs []string
|
|
if err := json.Unmarshal([]byte(issuesJSON), &issueIDs); err != nil {
|
|
return nil, fmt.Errorf("failed to unmarshal issues: %w", err)
|
|
}
|
|
|
|
if len(issueIDs) == 0 {
|
|
return []*models.Issue{}, nil
|
|
}
|
|
|
|
var issues []*models.Issue
|
|
for _, id := range issueIDs {
|
|
issue, err := s.Storage.GetIssue(ctx, id)
|
|
if err != nil {
|
|
// Skip issues that don't exist or can't be retrieved
|
|
continue
|
|
}
|
|
issues = append(issues, issue)
|
|
}
|
|
|
|
return issues, nil
|
|
}
|
|
|
|
func (s *BeadsService) AddIssueToSprint(ctx context.Context, issueID string, sprintNum int) error {
|
|
var issuesJSON string
|
|
err := s.UnderlyingDB().QueryRow("SELECT issues FROM sprints WHERE sprint_num = ?", sprintNum).Scan(&issuesJSON)
|
|
if err != nil {
|
|
if err == sql.ErrNoRows {
|
|
return fmt.Errorf("sprint %d not found", sprintNum)
|
|
}
|
|
return fmt.Errorf("failed to get sprint: %w", err)
|
|
}
|
|
|
|
var issueIDs []string
|
|
if err := json.Unmarshal([]byte(issuesJSON), &issueIDs); err != nil {
|
|
return fmt.Errorf("failed to unmarshal issues: %w", err)
|
|
}
|
|
|
|
if slices.Contains(issueIDs, issueID) {
|
|
return nil
|
|
}
|
|
|
|
issueIDs = append(issueIDs, issueID)
|
|
|
|
updatedJSON, err := json.Marshal(issueIDs)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to marshal issues: %w", err)
|
|
}
|
|
|
|
_, err = s.UnderlyingDB().Exec("UPDATE sprints SET issues = ? WHERE sprint_num = ?", string(updatedJSON), sprintNum)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to update sprint: %w", err)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (s *BeadsService) RemoveIssueFromSprint(ctx context.Context, issueID string, sprintNum int) error {
|
|
var issuesJSON string
|
|
err := s.UnderlyingDB().QueryRow("SELECT issues FROM sprints WHERE sprint_num = ?", sprintNum).Scan(&issuesJSON)
|
|
if err != nil {
|
|
if err == sql.ErrNoRows {
|
|
return fmt.Errorf("sprint %d not found", sprintNum)
|
|
}
|
|
return fmt.Errorf("failed to get sprint: %w", err)
|
|
}
|
|
|
|
var issueIDs []string
|
|
if err := json.Unmarshal([]byte(issuesJSON), &issueIDs); err != nil {
|
|
return fmt.Errorf("failed to unmarshal issues: %w", err)
|
|
}
|
|
|
|
found := false
|
|
var updatedIDs []string
|
|
for _, id := range issueIDs {
|
|
if id != issueID {
|
|
updatedIDs = append(updatedIDs, id)
|
|
} else {
|
|
found = true
|
|
}
|
|
}
|
|
|
|
if !found {
|
|
return nil
|
|
}
|
|
|
|
updatedJSON, err := json.Marshal(updatedIDs)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to marshal issues: %w", err)
|
|
}
|
|
|
|
_, err = s.UnderlyingDB().Exec("UPDATE sprints SET issues = ? WHERE sprint_num = ?", string(updatedJSON), sprintNum)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to update sprint: %w", err)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (s *BeadsService) GetBacklogSprint(ctx context.Context) (int, error) {
|
|
sprintNum, err := getBacklogSprintNum(s.Storage)
|
|
if err != nil {
|
|
if err == sql.ErrNoRows {
|
|
return 0, fmt.Errorf("backlog sprint not found")
|
|
}
|
|
return 0, fmt.Errorf("failed to get backlog sprint: %w", err)
|
|
}
|
|
return sprintNum, nil
|
|
}
|
|
|
|
// GetIssuesNotInAnySprint returns issues that are only in the backlog
|
|
func (s *BeadsService) GetIssuesNotInAnySprint(ctx context.Context) ([]*models.Issue, error) {
|
|
allIssues, err := s.Storage.SearchIssues(ctx, "", models.IssueFilter{})
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to get all issues: %w", err)
|
|
}
|
|
|
|
rows, err := s.UnderlyingDB().Query("SELECT sprint_num, issues FROM sprints WHERE is_backlog = 0")
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to get sprint issues: %w", err)
|
|
}
|
|
defer rows.Close()
|
|
|
|
issuesInSprints := make(map[string]bool)
|
|
for rows.Next() {
|
|
var sprintNum int
|
|
var issuesJSON string
|
|
if err := rows.Scan(&sprintNum, &issuesJSON); err != nil {
|
|
continue
|
|
}
|
|
var issueIDs []string
|
|
if err := json.Unmarshal([]byte(issuesJSON), &issueIDs); err != nil {
|
|
continue
|
|
}
|
|
for _, id := range issueIDs {
|
|
issuesInSprints[id] = true
|
|
}
|
|
}
|
|
|
|
var backlogIssues []*models.Issue
|
|
for _, issue := range allIssues {
|
|
if !issuesInSprints[issue.ID] {
|
|
backlogIssues = append(backlogIssues, issue)
|
|
}
|
|
}
|
|
|
|
return backlogIssues, nil
|
|
}
|
|
|
|
func getBacklogSprintNum(storage beads.Storage) (int, error) {
|
|
var sprintNum int
|
|
err := storage.UnderlyingDB().QueryRow("SELECT sprint_num FROM sprints WHERE is_backlog = 1 LIMIT 1").Scan(&sprintNum)
|
|
if err != nil {
|
|
return 0, err
|
|
}
|
|
return sprintNum, nil
|
|
}
|