1package db
2
3import (
4 "database/sql"
5 "time"
6
7 "github.com/bluesky-social/indigo/atproto/syntax"
8)
9
10type Issue struct {
11 RepoAt syntax.ATURI
12 OwnerDid string
13 IssueId int
14 IssueAt string
15 Created *time.Time
16 Title string
17 Body string
18 Open bool
19}
20
21type Comment struct {
22 OwnerDid string
23 RepoAt syntax.ATURI
24 CommentAt string
25 Issue int
26 CommentId int
27 Body string
28 Created *time.Time
29}
30
31func NewIssue(tx *sql.Tx, issue *Issue) error {
32 defer tx.Rollback()
33
34 _, err := tx.Exec(`
35 insert or ignore into repo_issue_seqs (repo_at, next_issue_id)
36 values (?, 1)
37 `, issue.RepoAt)
38 if err != nil {
39 return err
40 }
41
42 var nextId int
43 err = tx.QueryRow(`
44 update repo_issue_seqs
45 set next_issue_id = next_issue_id + 1
46 where repo_at = ?
47 returning next_issue_id - 1
48 `, issue.RepoAt).Scan(&nextId)
49 if err != nil {
50 return err
51 }
52
53 issue.IssueId = nextId
54
55 _, err = tx.Exec(`
56 insert into issues (repo_at, owner_did, issue_id, title, body)
57 values (?, ?, ?, ?, ?)
58 `, issue.RepoAt, issue.OwnerDid, issue.IssueId, issue.Title, issue.Body)
59 if err != nil {
60 return err
61 }
62
63 if err := tx.Commit(); err != nil {
64 return err
65 }
66
67 return nil
68}
69
70func SetIssueAt(e Execer, repoAt syntax.ATURI, issueId int, issueAt string) error {
71 _, err := e.Exec(`update issues set issue_at = ? where repo_at = ? and issue_id = ?`, issueAt, repoAt, issueId)
72 return err
73}
74
75func GetIssueAt(e Execer, repoAt syntax.ATURI, issueId int) (string, error) {
76 var issueAt string
77 err := e.QueryRow(`select issue_at from issues where repo_at = ? and issue_id = ?`, repoAt, issueId).Scan(&issueAt)
78 return issueAt, err
79}
80
81func GetIssueId(e Execer, repoAt syntax.ATURI) (int, error) {
82 var issueId int
83 err := e.QueryRow(`select next_issue_id from repo_issue_seqs where repo_at = ?`, repoAt).Scan(&issueId)
84 return issueId - 1, err
85}
86
87func GetIssueOwnerDid(e Execer, repoAt syntax.ATURI, issueId int) (string, error) {
88 var ownerDid string
89 err := e.QueryRow(`select owner_did from issues where repo_at = ? and issue_id = ?`, repoAt, issueId).Scan(&ownerDid)
90 return ownerDid, err
91}
92
93func GetIssues(e Execer, repoAt syntax.ATURI) ([]Issue, error) {
94 var issues []Issue
95
96 rows, err := e.Query(`select owner_did, issue_id, created, title, body, open from issues where repo_at = ? order by created desc`, repoAt)
97 if err != nil {
98 return nil, err
99 }
100 defer rows.Close()
101
102 for rows.Next() {
103 var issue Issue
104 var createdAt string
105 err := rows.Scan(&issue.OwnerDid, &issue.IssueId, &createdAt, &issue.Title, &issue.Body, &issue.Open)
106 if err != nil {
107 return nil, err
108 }
109
110 createdTime, err := time.Parse(time.RFC3339, createdAt)
111 if err != nil {
112 return nil, err
113 }
114 issue.Created = &createdTime
115
116 issues = append(issues, issue)
117 }
118
119 if err := rows.Err(); err != nil {
120 return nil, err
121 }
122
123 return issues, nil
124}
125
126func GetIssue(e Execer, repoAt syntax.ATURI, issueId int) (*Issue, error) {
127 query := `select owner_did, created, title, body, open from issues where repo_at = ? and issue_id = ?`
128 row := e.QueryRow(query, repoAt, issueId)
129
130 var issue Issue
131 var createdAt string
132 err := row.Scan(&issue.OwnerDid, &createdAt, &issue.Title, &issue.Body, &issue.Open)
133 if err != nil {
134 return nil, err
135 }
136
137 createdTime, err := time.Parse(time.RFC3339, createdAt)
138 if err != nil {
139 return nil, err
140 }
141 issue.Created = &createdTime
142
143 return &issue, nil
144}
145
146func GetIssueWithComments(e Execer, repoAt syntax.ATURI, issueId int) (*Issue, []Comment, error) {
147 query := `select owner_did, issue_id, created, title, body, open from issues where repo_at = ? and issue_id = ?`
148 row := e.QueryRow(query, repoAt, issueId)
149
150 var issue Issue
151 var createdAt string
152 err := row.Scan(&issue.OwnerDid, &issue.IssueId, &createdAt, &issue.Title, &issue.Body, &issue.Open)
153 if err != nil {
154 return nil, nil, err
155 }
156
157 createdTime, err := time.Parse(time.RFC3339, createdAt)
158 if err != nil {
159 return nil, nil, err
160 }
161 issue.Created = &createdTime
162
163 comments, err := GetComments(e, repoAt, issueId)
164 if err != nil {
165 return nil, nil, err
166 }
167
168 return &issue, comments, nil
169}
170
171func NewComment(e Execer, comment *Comment) error {
172 query := `insert into comments (owner_did, repo_at, comment_at, issue_id, comment_id, body) values (?, ?, ?, ?, ?, ?)`
173 _, err := e.Exec(
174 query,
175 comment.OwnerDid,
176 comment.RepoAt,
177 comment.CommentAt,
178 comment.Issue,
179 comment.CommentId,
180 comment.Body,
181 )
182 return err
183}
184
185func GetComments(e Execer, repoAt syntax.ATURI, issueId int) ([]Comment, error) {
186 var comments []Comment
187
188 rows, err := e.Query(`select owner_did, issue_id, comment_id, comment_at, body, created from comments where repo_at = ? and issue_id = ? order by created asc`, repoAt, issueId)
189 if err == sql.ErrNoRows {
190 return []Comment{}, nil
191 }
192 if err != nil {
193 return nil, err
194 }
195 defer rows.Close()
196
197 for rows.Next() {
198 var comment Comment
199 var createdAt string
200 err := rows.Scan(&comment.OwnerDid, &comment.Issue, &comment.CommentId, &comment.CommentAt, &comment.Body, &createdAt)
201 if err != nil {
202 return nil, err
203 }
204
205 createdAtTime, err := time.Parse(time.RFC3339, createdAt)
206 if err != nil {
207 return nil, err
208 }
209 comment.Created = &createdAtTime
210
211 comments = append(comments, comment)
212 }
213
214 if err := rows.Err(); err != nil {
215 return nil, err
216 }
217
218 return comments, nil
219}
220
221func CloseIssue(e Execer, repoAt syntax.ATURI, issueId int) error {
222 _, err := e.Exec(`update issues set open = 0 where repo_at = ? and issue_id = ?`, repoAt, issueId)
223 return err
224}
225
226func ReopenIssue(e Execer, repoAt syntax.ATURI, issueId int) error {
227 _, err := e.Exec(`update issues set open = 1 where repo_at = ? and issue_id = ?`, repoAt, issueId)
228 return err
229}
230
231type IssueCount struct {
232 Open int
233 Closed int
234}
235
236func GetIssueCount(e Execer, repoAt syntax.ATURI) (IssueCount, error) {
237 row := e.QueryRow(`
238 select
239 count(case when open = 1 then 1 end) as open_count,
240 count(case when open = 0 then 1 end) as closed_count
241 from issues
242 where repo_at = ?`,
243 repoAt,
244 )
245
246 var count IssueCount
247 if err := row.Scan(&count.Open, &count.Closed); err != nil {
248 return IssueCount{0, 0}, err
249 }
250
251 return count, nil
252}