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