← back to kjm99d__MonkeyPlanner

Function bodies 254 total

All specs Real LLM only Function bodies
DeleteIssue method · go · L147-L155 (9 LOC)
backend/internal/service/service.go
func (s *Service) DeleteIssue(ctx context.Context, id string) error {
	iss, _ := s.repo.Issues().GetByID(ctx, id)
	err := s.repo.Issues().Delete(ctx, id)
	if err != nil {
		return err
	}
	s.DispatchWebhook(iss.BoardID, domain.EventIssueDeleted, &iss)
	return nil
}
GetIssue method · go · L157-L164 (8 LOC)
backend/internal/service/service.go
func (s *Service) GetIssue(ctx context.Context, id string) (issue domain.Issue, children []domain.Issue, err error) {
	issue, err = s.repo.Issues().GetByID(ctx, id)
	if err != nil {
		return
	}
	children, err = s.repo.Issues().ListChildren(ctx, id)
	return
}
ListIssues method · go · L166-L168 (3 LOC)
backend/internal/service/service.go
func (s *Service) ListIssues(ctx context.Context, f storage.IssueFilter) ([]domain.Issue, error) {
	return s.repo.Issues().List(ctx, f)
}
ReorderIssues method · go · L170-L172 (3 LOC)
backend/internal/service/service.go
func (s *Service) ReorderIssues(ctx context.Context, issueIDs []string) error {
	return s.repo.Issues().ReorderIssues(ctx, issueIDs)
}
AddDependency method · go · L174-L176 (3 LOC)
backend/internal/service/service.go
func (s *Service) AddDependency(ctx context.Context, blockerID, blockedID string) error {
	return s.repo.Issues().AddDependency(ctx, blockerID, blockedID)
}
RemoveDependency method · go · L178-L180 (3 LOC)
backend/internal/service/service.go
func (s *Service) RemoveDependency(ctx context.Context, blockerID, blockedID string) error {
	return s.repo.Issues().RemoveDependency(ctx, blockerID, blockedID)
}
GetBlockedBy method · go · L182-L184 (3 LOC)
backend/internal/service/service.go
func (s *Service) GetBlockedBy(ctx context.Context, issueID string) ([]string, error) {
	return s.repo.Issues().GetBlockedBy(ctx, issueID)
}
Same scanner, your repo: https://repobility.com — Repobility
CreateComment method · go · L188-L190 (3 LOC)
backend/internal/service/service.go
func (s *Service) CreateComment(ctx context.Context, issueID, body string) (*domain.Comment, error) {
	return s.repo.Comments().Create(ctx, issueID, body)
}
ListComments method · go · L192-L194 (3 LOC)
backend/internal/service/service.go
func (s *Service) ListComments(ctx context.Context, issueID string) ([]domain.Comment, error) {
	return s.repo.Comments().List(ctx, issueID)
}
DeleteComment method · go · L196-L198 (3 LOC)
backend/internal/service/service.go
func (s *Service) DeleteComment(ctx context.Context, commentID string) error {
	return s.repo.Comments().Delete(ctx, commentID)
}
CreateBoard method · go · L202-L219 (18 LOC)
backend/internal/service/service.go
func (s *Service) CreateBoard(ctx context.Context, name string, viewType domain.ViewType) (domain.Board, error) {
	if name == "" {
		return domain.Board{}, errors.New("board name must not be empty")
	}
	if viewType == "" {
		viewType = domain.ViewKanban
	}
	if !viewType.Valid() {
		return domain.Board{}, errors.New("invalid viewType")
	}
	b := domain.Board{
		ID:        uuid.NewString(),
		Name:      name,
		ViewType:  viewType,
		CreatedAt: s.now(),
	}
	return s.repo.Boards().Create(ctx, b)
}
ListBoards method · go · L221-L223 (3 LOC)
backend/internal/service/service.go
func (s *Service) ListBoards(ctx context.Context) ([]domain.Board, error) {
	return s.repo.Boards().List(ctx)
}
GetBoard method · go · L225-L227 (3 LOC)
backend/internal/service/service.go
func (s *Service) GetBoard(ctx context.Context, id string) (domain.Board, error) {
	return s.repo.Boards().GetByID(ctx, id)
}
UpdateBoard method · go · L229-L234 (6 LOC)
backend/internal/service/service.go
func (s *Service) UpdateBoard(ctx context.Context, id string, name *string, viewType *domain.ViewType) (domain.Board, error) {
	if viewType != nil && !viewType.Valid() {
		return domain.Board{}, errors.New("invalid viewType")
	}
	return s.repo.Boards().Update(ctx, id, name, viewType)
}
DeleteBoard method · go · L236-L238 (3 LOC)
backend/internal/service/service.go
func (s *Service) DeleteBoard(ctx context.Context, id string) error {
	return s.repo.Boards().Delete(ctx, id)
}
Powered by Repobility — scan your code at https://repobility.com
UpdateIssueProperties method · go · L243-L260 (18 LOC)
backend/internal/service/service.go
func (s *Service) UpdateIssueProperties(ctx context.Context, id string, props map[string]any) (domain.Issue, error) {
	cur, err := s.repo.Issues().GetByID(ctx, id)
	if err != nil {
		return domain.Issue{}, err
	}
	if cur.Properties == nil {
		cur.Properties = map[string]any{}
	}
	for k, v := range props {
		if v == nil {
			delete(cur.Properties, k)
		} else {
			cur.Properties[k] = v
		}
	}
	merged := cur.Properties
	return s.repo.Issues().Update(ctx, id, storage.IssuePatch{Properties: &merged})
}
CreateBoardProperty method · go · L264-L283 (20 LOC)
backend/internal/service/service.go
func (s *Service) CreateBoardProperty(ctx context.Context, boardID, name string, propType domain.PropertyType, options []string) (domain.BoardProperty, error) {
	if name == "" {
		return domain.BoardProperty{}, errors.New("property name must not be empty")
	}
	if !propType.Valid() {
		return domain.BoardProperty{}, errors.New("invalid property type")
	}
	if options == nil {
		options = []string{}
	}
	existing, _ := s.repo.BoardProperties().List(ctx, boardID)
	p := domain.BoardProperty{
		BoardID:  boardID,
		Name:     name,
		Type:     propType,
		Options:  options,
		Position: len(existing),
	}
	return s.repo.BoardProperties().Create(ctx, p)
}
ListBoardProperties method · go · L285-L287 (3 LOC)
backend/internal/service/service.go
func (s *Service) ListBoardProperties(ctx context.Context, boardID string) ([]domain.BoardProperty, error) {
	return s.repo.BoardProperties().List(ctx, boardID)
}
UpdateBoardProperty method · go · L289-L291 (3 LOC)
backend/internal/service/service.go
func (s *Service) UpdateBoardProperty(ctx context.Context, id string, name *string, options *[]string, position *int) (domain.BoardProperty, error) {
	return s.repo.BoardProperties().Update(ctx, id, name, options, position)
}
DeleteBoardProperty method · go · L293-L295 (3 LOC)
backend/internal/service/service.go
func (s *Service) DeleteBoardProperty(ctx context.Context, id string) error {
	return s.repo.BoardProperties().Delete(ctx, id)
}
GetMonthStats method · go · L299-L301 (3 LOC)
backend/internal/service/service.go
func (s *Service) GetMonthStats(ctx context.Context, year int, month time.Month) ([]storage.DayCount, error) {
	return s.repo.Issues().GetMonthStats(ctx, year, month)
}
GetDayStats method · go · L303-L305 (3 LOC)
backend/internal/service/service.go
func (s *Service) GetDayStats(ctx context.Context, day time.Time) (storage.DayStats, error) {
	return s.repo.Issues().GetDayStats(ctx, day)
}
CreateWebhook method · go · L17-L37 (21 LOC)
backend/internal/service/webhook.go
func (s *Service) CreateWebhook(ctx context.Context, boardID, name, url string, events []domain.WebhookEvent) (domain.Webhook, error) {
	if name == "" {
		return domain.Webhook{}, errors.New("webhook name must not be empty")
	}
	if url == "" {
		return domain.Webhook{}, errors.New("webhook url must not be empty")
	}
	for _, e := range events {
		if !e.Valid() {
			return domain.Webhook{}, errors.New("invalid webhook event: " + string(e))
		}
	}
	wh := domain.Webhook{
		BoardID: boardID,
		Name:    name,
		URL:     url,
		Events:  events,
		Enabled: true,
	}
	return s.repo.Webhooks().Create(ctx, wh)
}
Source: Repobility analyzer · https://repobility.com
ListWebhooks method · go · L39-L41 (3 LOC)
backend/internal/service/webhook.go
func (s *Service) ListWebhooks(ctx context.Context, boardID string) ([]domain.Webhook, error) {
	return s.repo.Webhooks().List(ctx, boardID)
}
UpdateWebhook method · go · L43-L45 (3 LOC)
backend/internal/service/webhook.go
func (s *Service) UpdateWebhook(ctx context.Context, id string, name *string, url *string, events *[]domain.WebhookEvent, enabled *bool) (domain.Webhook, error) {
	return s.repo.Webhooks().Update(ctx, id, name, url, events, enabled)
}
DeleteWebhook method · go · L47-L49 (3 LOC)
backend/internal/service/webhook.go
func (s *Service) DeleteWebhook(ctx context.Context, id string) error {
	return s.repo.Webhooks().Delete(ctx, id)
}
DispatchWebhook method · go · L53-L91 (39 LOC)
backend/internal/service/webhook.go
func (s *Service) DispatchWebhook(boardID string, event domain.WebhookEvent, issue *domain.Issue) {
	go func() {
		ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
		defer cancel()

		hooks, err := s.repo.Webhooks().ListByEvent(ctx, boardID, event)
		if err != nil || len(hooks) == 0 {
			return
		}

		board, _ := s.repo.Boards().GetByID(ctx, boardID)

		payload := domain.WebhookPayload{
			Event:     event,
			Issue:     issue,
			Board:     &board,
			Timestamp: s.now(),
		}
		body, _ := json.Marshal(payload)

		client := &http.Client{Timeout: 5 * time.Second}
		for _, wh := range hooks {
			req, err := http.NewRequestWithContext(ctx, http.MethodPost, wh.URL, bytes.NewReader(body))
			if err != nil {
				log.Printf("webhook %s: request error: %v", wh.Name, err)
				continue
			}
			req.Header.Set("Content-Type", "application/json")
			req.Header.Set("X-Webhook-Event", string(event))
			resp, err := client.Do(req)
			if err != nil {
				log.Printf("webhook %s: s
RunAll function · go · L20-L39 (20 LOC)
backend/internal/storage/contract/contract.go
func RunAll(t *testing.T, newRepo func(t *testing.T) storage.Repo) {
	t.Helper()
	cases := []struct {
		name string
		fn   func(t *testing.T, repo storage.Repo)
	}{
		{"CreateAndGet", testCreateAndGet},
		{"ApproveIdempotent", testApproveIdempotent},
		{"CycleDetection", testCycleDetection},
		{"CascadeDelete", testCascadeDelete},
		{"MonthStats", testMonthStats},
	}
	for _, tc := range cases {
		tc := tc
		t.Run(tc.name, func(t *testing.T) {
			repo := newRepo(t)
			tc.fn(t, repo)
		})
	}
}
seedBoard function · go · L41-L53 (13 LOC)
backend/internal/storage/contract/contract.go
func seedBoard(t *testing.T, repo storage.Repo) domain.Board {
	t.Helper()
	b := domain.Board{
		ID:        uuid.NewString(),
		Name:      "contract-board",
		ViewType:  domain.ViewKanban,
		CreatedAt: time.Now().UTC(),
	}
	if _, err := repo.Boards().Create(context.Background(), b); err != nil {
		t.Fatalf("seed board: %v", err)
	}
	return b
}
mkIssue function · go · L55-L61 (7 LOC)
backend/internal/storage/contract/contract.go
func mkIssue(boardID string) domain.Issue {
	now := time.Now().UTC()
	return domain.Issue{
		ID: uuid.NewString(), BoardID: boardID, Title: "t",
		Status: domain.StatusPending, CreatedAt: now, UpdatedAt: now,
	}
}
testCreateAndGet function · go · L63-L77 (15 LOC)
backend/internal/storage/contract/contract.go
func testCreateAndGet(t *testing.T, repo storage.Repo) {
	ctx := context.Background()
	b := seedBoard(t, repo)
	iss := mkIssue(b.ID)
	if _, err := repo.Issues().Create(ctx, iss); err != nil {
		t.Fatalf("create: %v", err)
	}
	got, err := repo.Issues().GetByID(ctx, iss.ID)
	if err != nil {
		t.Fatalf("get: %v", err)
	}
	if got.Status != domain.StatusPending {
		t.Errorf("status: got=%s want=Pending", got.Status)
	}
}
If a scraper extracted this row, it came from Repobility (https://repobility.com)
testApproveIdempotent function · go · L79-L100 (22 LOC)
backend/internal/storage/contract/contract.go
func testApproveIdempotent(t *testing.T, repo storage.Repo) {
	ctx := context.Background()
	b := seedBoard(t, repo)
	iss := mkIssue(b.ID)
	if _, err := repo.Issues().Create(ctx, iss); err != nil {
		t.Fatal(err)
	}
	now := time.Now().UTC()
	first, err := repo.Issues().Approve(ctx, iss.ID, now)
	if err != nil || first.ApprovedAt == nil {
		t.Fatalf("approve 1: err=%v approved=%v", err, first.ApprovedAt)
	}
	firstApproved := *first.ApprovedAt
	time.Sleep(15 * time.Millisecond)
	second, err := repo.Issues().Approve(ctx, iss.ID, time.Now().UTC())
	if err != nil {
		t.Fatalf("approve 2: %v", err)
	}
	if !second.ApprovedAt.Equal(firstApproved) {
		t.Fatalf("not idempotent: first=%v second=%v", firstApproved, *second.ApprovedAt)
	}
}
testCycleDetection function · go · L102-L134 (33 LOC)
backend/internal/storage/contract/contract.go
func testCycleDetection(t *testing.T, repo storage.Repo) {
	ctx := context.Background()
	b := seedBoard(t, repo)
	ir := repo.Issues()

	create := func(title string) domain.Issue {
		i := mkIssue(b.ID)
		i.Title = title
		if _, err := ir.Create(ctx, i); err != nil {
			t.Fatalf("create %s: %v", title, err)
		}
		return i
	}
	a, bi, c := create("A"), create("B"), create("C")
	setParent := func(child, parent string) error {
		p := &parent
		pp := &p
		_, err := ir.Update(ctx, child, storage.IssuePatch{ParentID: pp})
		return err
	}
	if err := setParent(bi.ID, a.ID); err != nil {
		t.Fatalf("B→A: %v", err)
	}
	if err := setParent(c.ID, bi.ID); err != nil {
		t.Fatalf("C→B: %v", err)
	}
	if err := setParent(a.ID, c.ID); !errors.Is(err, storage.ErrCycle) {
		t.Fatalf("cycle A→C expected ErrCycle, got %v", err)
	}
	if err := setParent(a.ID, a.ID); !errors.Is(err, storage.ErrCycle) {
		t.Fatalf("self parent expected ErrCycle, got %v", err)
	}
}
testCascadeDelete function · go · L136-L156 (21 LOC)
backend/internal/storage/contract/contract.go
func testCascadeDelete(t *testing.T, repo storage.Repo) {
	ctx := context.Background()
	b := seedBoard(t, repo)
	ir := repo.Issues()
	parent := mkIssue(b.ID)
	if _, err := ir.Create(ctx, parent); err != nil {
		t.Fatal(err)
	}
	pid := parent.ID
	child := mkIssue(b.ID)
	child.ParentID = &pid
	if _, err := ir.Create(ctx, child); err != nil {
		t.Fatal(err)
	}
	if err := ir.Delete(ctx, parent.ID); err != nil {
		t.Fatal(err)
	}
	if _, err := ir.GetByID(ctx, child.ID); !errors.Is(err, storage.ErrNotFound) {
		t.Fatalf("cascade failed: %v", err)
	}
}
testMonthStats function · go · L158-L185 (28 LOC)
backend/internal/storage/contract/contract.go
func testMonthStats(t *testing.T, repo storage.Repo) {
	ctx := context.Background()
	b := seedBoard(t, repo)
	ir := repo.Issues()
	day := time.Now().UTC()
	iss := mkIssue(b.ID)
	iss.CreatedAt = day
	iss.UpdatedAt = day
	if _, err := ir.Create(ctx, iss); err != nil {
		t.Fatal(err)
	}
	if _, err := ir.Approve(ctx, iss.ID, day); err != nil {
		t.Fatal(err)
	}
	stats, err := ir.GetMonthStats(ctx, day.Year(), day.Month())
	if err != nil {
		t.Fatal(err)
	}
	var seen bool
	for _, d := range stats {
		if d.Date.Day() == day.Day() && d.Created >= 1 && d.Approved >= 1 {
			seen = true
		}
	}
	if !seen {
		t.Fatalf("expected created/approved for today, got %+v", stats)
	}
}
Register function · go · L20-L22 (3 LOC)
backend/internal/storage/factory.go
func Register(scheme string, o Opener) {
	openers[scheme] = o
}
NewRepo function · go · L25-L32 (8 LOC)
backend/internal/storage/factory.go
func NewRepo(dsn string) (Repo, error) {
	scheme := dsnScheme(dsn)
	o, ok := openers[scheme]
	if !ok {
		return nil, fmt.Errorf("storage: unsupported DSN scheme %q (registered: %v)", scheme, keys(openers))
	}
	return o(dsn)
}
keys function · go · L45-L51 (7 LOC)
backend/internal/storage/factory.go
func keys(m map[string]Opener) []string {
	out := make([]string, 0, len(m))
	for k := range m {
		out = append(out, k)
	}
	return out
}
init function · go · L5-L9 (5 LOC)
backend/internal/storage/postgres/init.go
func init() {
	storage.Register("postgres", func(dsn string) (storage.Repo, error) {
		return Open(dsn)
	})
}
Same scanner, your repo: https://repobility.com — Repobility
runMigrations function · go · L14-L23 (10 LOC)
backend/internal/storage/postgres/migrations.go
func runMigrations(db *sql.DB) error {
	goose.SetBaseFS(migrationsFS)
	if err := goose.SetDialect("postgres"); err != nil {
		return fmt.Errorf("postgres: set dialect: %w", err)
	}
	if err := goose.Up(db, "migrations"); err != nil {
		return fmt.Errorf("postgres: goose up: %w", err)
	}
	return nil
}
Open function · go · L28-L46 (19 LOC)
backend/internal/storage/postgres/repo.go
func Open(dsn string) (*Repo, error) {
	db, err := sql.Open("pgx", dsn)
	if err != nil {
		return nil, fmt.Errorf("postgres open: %w", err)
	}
	if err := db.Ping(); err != nil {
		_ = db.Close()
		return nil, fmt.Errorf("postgres ping: %w", err)
	}
	if err := runMigrations(db); err != nil {
		_ = db.Close()
		return nil, err
	}
	return &Repo{
		db:     db,
		issues: &issueRepo{db: db},
		boards: &boardRepo{db: db},
	}, nil
}
Create method · go · L61-L75 (15 LOC)
backend/internal/storage/postgres/repo.go
func (r *issueRepo) Create(ctx context.Context, i domain.Issue) (domain.Issue, error) {
	if i.Criteria == nil {
		i.Criteria = []domain.Criterion{}
	}
	criteriaJSON, _ := json.Marshal(i.Criteria)
	_, err := r.db.ExecContext(ctx, `
		INSERT INTO issues (id, board_id, parent_id, title, body, instructions, status, criteria, created_at, updated_at, approved_at, completed_at)
		VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9,$10,$11,$12)`,
		i.ID, i.BoardID, i.ParentID, i.Title, i.Body, i.Instructions, i.Status, string(criteriaJSON),
		i.CreatedAt.UTC(), i.UpdatedAt.UTC(), utcPtr(i.ApprovedAt), utcPtr(i.CompletedAt))
	if err != nil {
		return domain.Issue{}, fmt.Errorf("postgres: create issue: %w", err)
	}
	return i, nil
}
GetByID method · go · L77-L79 (3 LOC)
backend/internal/storage/postgres/repo.go
func (r *issueRepo) GetByID(ctx context.Context, id string) (domain.Issue, error) {
	return scanIssue(r.db.QueryRowContext(ctx, selectIssueCols+` WHERE id = $1`, id))
}
ListChildren method · go · L81-L88 (8 LOC)
backend/internal/storage/postgres/repo.go
func (r *issueRepo) ListChildren(ctx context.Context, parentID string) ([]domain.Issue, error) {
	rows, err := r.db.QueryContext(ctx, selectIssueCols+` WHERE parent_id = $1 ORDER BY created_at ASC`, parentID)
	if err != nil {
		return nil, fmt.Errorf("postgres: list children: %w", err)
	}
	defer rows.Close()
	return collectIssues(rows)
}
List method · go · L90-L120 (31 LOC)
backend/internal/storage/postgres/repo.go
func (r *issueRepo) List(ctx context.Context, f storage.IssueFilter) ([]domain.Issue, error) {
	q := selectIssueCols + ` WHERE 1=1`
	var args []any
	i := 1
	addArg := func(v any) string {
		args = append(args, v)
		p := fmt.Sprintf("$%d", i)
		i++
		return p
	}
	if f.BoardID != nil {
		q += ` AND board_id = ` + addArg(*f.BoardID)
	}
	if f.Status != nil {
		q += ` AND status = ` + addArg(string(*f.Status))
	}
	if f.ParentID != nil {
		if *f.ParentID == "" {
			q += ` AND parent_id IS NULL`
		} else {
			q += ` AND parent_id = ` + addArg(*f.ParentID)
		}
	}
	q += ` ORDER BY position ASC, created_at DESC`
	rows, err := r.db.QueryContext(ctx, q, args...)
	if err != nil {
		return nil, fmt.Errorf("postgres: list issues: %w", err)
	}
	defer rows.Close()
	return collectIssues(rows)
}
Update method · go · L122-L181 (60 LOC)
backend/internal/storage/postgres/repo.go
func (r *issueRepo) Update(ctx context.Context, id string, p storage.IssuePatch) (domain.Issue, error) {
	tx, err := r.db.BeginTx(ctx, nil)
	if err != nil {
		return domain.Issue{}, fmt.Errorf("postgres: begin: %w", err)
	}
	defer tx.Rollback()

	cur, err := scanIssue(tx.QueryRowContext(ctx, selectIssueCols+` WHERE id = $1`, id))
	if err != nil {
		return domain.Issue{}, err
	}

	if p.Title != nil {
		cur.Title = *p.Title
	}
	if p.Body != nil {
		cur.Body = *p.Body
	}
	if p.Instructions != nil {
		cur.Instructions = *p.Instructions
	}
	if p.Status != nil {
		cur.Status = *p.Status
	}
	if p.Criteria != nil {
		cur.Criteria = *p.Criteria
	}
	if p.ParentID != nil {
		newParent := *p.ParentID
		if newParent != nil {
			if *newParent == id {
				return domain.Issue{}, storage.ErrCycle
			}
			hasCycle, err := detectCycleTx(ctx, tx, id, *newParent)
			if err != nil {
				return domain.Issue{}, err
			}
			if hasCycle {
				return domain.Issue{}, storage.ErrCycle
			}
			cur.ParentID = newPar
Delete method · go · L183-L193 (11 LOC)
backend/internal/storage/postgres/repo.go
func (r *issueRepo) Delete(ctx context.Context, id string) error {
	res, err := r.db.ExecContext(ctx, `DELETE FROM issues WHERE id = $1`, id)
	if err != nil {
		return fmt.Errorf("postgres: delete issue: %w", err)
	}
	n, _ := res.RowsAffected()
	if n == 0 {
		return storage.ErrNotFound
	}
	return nil
}
Powered by Repobility — scan your code at https://repobility.com
Approve method · go · L195-L207 (13 LOC)
backend/internal/storage/postgres/repo.go
func (r *issueRepo) Approve(ctx context.Context, id string, now time.Time) (domain.Issue, error) {
	_, err := r.db.ExecContext(ctx, `
		UPDATE issues
		SET status='Approved',
		    approved_at = COALESCE(approved_at, $1),
		    updated_at = $2
		WHERE id = $3 AND status IN ('Pending','Approved')`,
		now.UTC(), now.UTC(), id)
	if err != nil {
		return domain.Issue{}, fmt.Errorf("postgres: approve: %w", err)
	}
	return r.GetByID(ctx, id)
}
Complete method · go · L209-L232 (24 LOC)
backend/internal/storage/postgres/repo.go
func (r *issueRepo) Complete(ctx context.Context, id string, now time.Time) (domain.Issue, error) {
	res, err := r.db.ExecContext(ctx, `
		UPDATE issues
		SET status='Done',
		    completed_at = COALESCE(completed_at, $1),
		    updated_at = $2
		WHERE id = $3 AND status = 'InProgress'`,
		now.UTC(), now.UTC(), id)
	if err != nil {
		return domain.Issue{}, fmt.Errorf("postgres: complete: %w", err)
	}
	n, _ := res.RowsAffected()
	if n == 0 {
		cur, err := r.GetByID(ctx, id)
		if err != nil {
			return domain.Issue{}, err
		}
		if cur.Status == domain.StatusDone {
			return cur, nil
		}
		return domain.Issue{}, storage.ErrConflict
	}
	return r.GetByID(ctx, id)
}
GetMonthStats method · go · L234-L268 (35 LOC)
backend/internal/storage/postgres/repo.go
func (r *issueRepo) GetMonthStats(ctx context.Context, year int, month time.Month) ([]storage.DayCount, error) {
	start := time.Date(year, month, 1, 0, 0, 0, 0, time.UTC)
	end := start.AddDate(0, 1, 0)
	q := `
		WITH days AS (
			SELECT (created_at::date) AS d, 1 AS c, 0 AS a, 0 AS f FROM issues
			WHERE created_at >= $1 AND created_at < $2
			UNION ALL
			SELECT (approved_at::date), 0, 1, 0 FROM issues
			WHERE approved_at IS NOT NULL AND approved_at >= $1 AND approved_at < $2
			UNION ALL
			SELECT (completed_at::date), 0, 0, 1 FROM issues
			WHERE completed_at IS NOT NULL AND completed_at >= $1 AND completed_at < $2
		)
		SELECT d, SUM(c)::int, SUM(a)::int, SUM(f)::int
		FROM days
		GROUP BY d
		ORDER BY d`
	rows, err := r.db.QueryContext(ctx, q, start, end)
	if err != nil {
		return nil, fmt.Errorf("postgres: month stats: %w", err)
	}
	defer rows.Close()
	var out []storage.DayCount
	for rows.Next() {
		var d time.Time
		var c storage.DayCount
		if err := rows.Scan(&d, &c.Created, &
‹ prevpage 2 / 6next ›