internal/store/dashboard.go
115 lines · 3630 bytes
1package store
2
3// DashboardItem is one open issue or MR row on the logged-in homepage.
4type DashboardItem struct {
5 RepoPath string
6 Number int64
7 Title string
8 Author string
9 State string
10 UpdatedAt string
11}
12
13// involvedRepos filters to repositories the user owns, is granted on, or
14// reaches through org membership — or rows the user authored anywhere.
15const involvedCond = `(
16 x.author_id = ?1
17 OR (r.owner_kind = 'user' AND r.owner_id = ?1)
18 OR EXISTS (SELECT 1 FROM repo_access a
19 WHERE a.repo_id = r.id AND a.subject_kind = 'user' AND a.subject_id = ?1)
20 OR EXISTS (SELECT 1 FROM org_members mm
21 WHERE r.owner_kind = 'org' AND mm.org_id = r.owner_id AND mm.user_id = ?1)
22)`
23
24func (s *Store) dashboardQuery(q string, userID int64) ([]DashboardItem, error) {
25 rows, err := s.DB.Query(q, userID)
26 if err != nil {
27 return nil, err
28 }
29 defer rows.Close()
30 var out []DashboardItem
31 for rows.Next() {
32 var d DashboardItem
33 if err := rows.Scan(&d.RepoPath, &d.Number, &d.Title, &d.Author, &d.State, &d.UpdatedAt); err != nil {
34 return nil, err
35 }
36 out = append(out, d)
37 }
38 return out, rows.Err()
39}
40
41// DashboardMRs returns open merge requests involving the user: on their
42// repositories (owned, granted, org) or authored by them anywhere.
43func (s *Store) DashboardMRs(userID int64) ([]DashboardItem, error) {
44 return s.dashboardQuery(`
45 SELECT COALESCE(u.username, o.name) || '/' || r.name,
46 x.number, x.title, au.username, x.state, x.updated_at
47 FROM merge_requests x
48 JOIN repos r ON r.id = x.repo_id
49 LEFT JOIN users u ON r.owner_kind = 'user' AND u.id = r.owner_id
50 LEFT JOIN orgs o ON r.owner_kind = 'org' AND o.id = r.owner_id
51 JOIN users au ON au.id = x.author_id
52 WHERE x.state IN ('open', 'source_gone') AND `+involvedCond+`
53 ORDER BY x.updated_at DESC LIMIT 50`, userID)
54}
55
56// DashboardIssues is the issue counterpart of DashboardMRs.
57func (s *Store) DashboardIssues(userID int64) ([]DashboardItem, error) {
58 return s.dashboardQuery(`
59 SELECT COALESCE(u.username, o.name) || '/' || r.name,
60 x.number, x.title, au.username, x.state, x.updated_at
61 FROM issues x
62 JOIN repos r ON r.id = x.repo_id
63 LEFT JOIN users u ON r.owner_kind = 'user' AND u.id = r.owner_id
64 LEFT JOIN orgs o ON r.owner_kind = 'org' AND o.id = r.owner_id
65 JOIN users au ON au.id = x.author_id
66 WHERE x.state = 'open' AND `+involvedCond+`
67 ORDER BY x.updated_at DESC LIMIT 50`, userID)
68}
69
70func (s *Store) PinRepo(userID, repoID int64) error {
71 _, err := s.DB.Exec(
72 "INSERT INTO repo_pins (user_id, repo_id) VALUES (?, ?) ON CONFLICT DO NOTHING",
73 userID, repoID)
74 return err
75}
76
77func (s *Store) IsPinned(userID, repoID int64) bool {
78 var n int
79 s.DB.QueryRow("SELECT COUNT(*) FROM repo_pins WHERE user_id = ? AND repo_id = ?",
80 userID, repoID).Scan(&n)
81 return n > 0
82}
83
84func (s *Store) UnpinRepo(userID, repoID int64) error {
85 res, err := s.DB.Exec(
86 "DELETE FROM repo_pins WHERE user_id = ? AND repo_id = ?", userID, repoID)
87 if err != nil {
88 return err
89 }
90 if n, _ := res.RowsAffected(); n == 0 {
91 return ErrNotFound
92 }
93 return nil
94}
95
96// PinnedRepos returns the user's pinned repositories in pin order. The
97// caller applies visibility checks before rendering.
98func (s *Store) PinnedRepos(userID int64) ([]Repo, error) {
99 rows, err := s.DB.Query(repoSelect+`
100 JOIN repo_pins p ON p.repo_id = r.id AND p.user_id = ?
101 ORDER BY p.pinned_at`, userID)
102 if err != nil {
103 return nil, err
104 }
105 defer rows.Close()
106 var out []Repo
107 for rows.Next() {
108 r, err := scanRepo(rows)
109 if err != nil {
110 return nil, err
111 }
112 out = append(out, r)
113 }
114 return out, rows.Err()
115}