forked from
tangled.org/core
fork
Configure Feed
Select the types of activity you want to include in your feed.
this repo has no description
fork
Configure Feed
Select the types of activity you want to include in your feed.
1package db
2
3import (
4 "database/sql"
5 "time"
6
7 "github.com/bluesky-social/indigo/atproto/syntax"
8 "tangled.sh/tangled.sh/core/appview/pagination"
9)
10
11type Issue struct {
12 ID int64
13 RepoAt syntax.ATURI
14 OwnerDid string
15 IssueId int
16 IssueAt string
17 Created time.Time
18 Title string
19 Body string
20 Open bool
21
22 // optionally, populate this when querying for reverse mappings
23 // like comment counts, parent repo etc.
24 Metadata *IssueMetadata
25}
26
27type IssueMetadata struct {
28 CommentCount int
29 Repo *Repo
30 // labels, assignee etc.
31}
32
33type Comment struct {
34 OwnerDid string
35 RepoAt syntax.ATURI
36 Rkey string
37 Issue int
38 CommentId int
39 Body string
40 Created *time.Time
41 Deleted *time.Time
42 Edited *time.Time
43}
44
45func NewIssue(tx *sql.Tx, issue *Issue) error {
46 defer tx.Rollback()
47
48 _, err := tx.Exec(`
49 insert or ignore into repo_issue_seqs (repo_at, next_issue_id)
50 values (?, 1)
51 `, issue.RepoAt)
52 if err != nil {
53 return err
54 }
55
56 var nextId int
57 err = tx.QueryRow(`
58 update repo_issue_seqs
59 set next_issue_id = next_issue_id + 1
60 where repo_at = ?
61 returning next_issue_id - 1
62 `, issue.RepoAt).Scan(&nextId)
63 if err != nil {
64 return err
65 }
66
67 issue.IssueId = nextId
68
69 res, err := tx.Exec(`
70 insert into issues (repo_at, owner_did, issue_id, title, body)
71 values (?, ?, ?, ?, ?)
72 `, issue.RepoAt, issue.OwnerDid, issue.IssueId, issue.Title, issue.Body)
73 if err != nil {
74 return err
75 }
76
77 lastID, err := res.LastInsertId()
78 if err != nil {
79 return err
80 }
81 issue.ID = lastID
82
83 if err := tx.Commit(); err != nil {
84 return err
85 }
86
87 return nil
88}
89
90func SetIssueAt(e Execer, repoAt syntax.ATURI, issueId int, issueAt string) error {
91 _, err := e.Exec(`update issues set issue_at = ? where repo_at = ? and issue_id = ?`, issueAt, repoAt, issueId)
92 return err
93}
94
95func GetIssueAt(e Execer, repoAt syntax.ATURI, issueId int) (string, error) {
96 var issueAt string
97 err := e.QueryRow(`select issue_at from issues where repo_at = ? and issue_id = ?`, repoAt, issueId).Scan(&issueAt)
98 return issueAt, err
99}
100
101func GetIssueOwnerDid(e Execer, repoAt syntax.ATURI, issueId int) (string, error) {
102 var ownerDid string
103 err := e.QueryRow(`select owner_did from issues where repo_at = ? and issue_id = ?`, repoAt, issueId).Scan(&ownerDid)
104 return ownerDid, err
105}
106
107func GetIssues(e Execer, repoAt syntax.ATURI, isOpen bool, page pagination.Page) ([]Issue, error) {
108 var issues []Issue
109 openValue := 0
110 if isOpen {
111 openValue = 1
112 }
113
114 rows, err := e.Query(
115 `
116 with numbered_issue as (
117 select
118 i.id,
119 i.owner_did,
120 i.issue_id,
121 i.created,
122 i.title,
123 i.body,
124 i.open,
125 count(c.id) as comment_count,
126 row_number() over (order by i.created desc) as row_num
127 from
128 issues i
129 left join
130 comments c on i.repo_at = c.repo_at and i.issue_id = c.issue_id
131 where
132 i.repo_at = ? and i.open = ?
133 group by
134 i.id, i.owner_did, i.issue_id, i.created, i.title, i.body, i.open
135 )
136 select
137 id,
138 owner_did,
139 issue_id,
140 created,
141 title,
142 body,
143 open,
144 comment_count
145 from
146 numbered_issue
147 where
148 row_num between ? and ?`,
149 repoAt, openValue, page.Offset+1, page.Offset+page.Limit)
150 if err != nil {
151 return nil, err
152 }
153 defer rows.Close()
154
155 for rows.Next() {
156 var issue Issue
157 var createdAt string
158 var metadata IssueMetadata
159 err := rows.Scan(&issue.ID, &issue.OwnerDid, &issue.IssueId, &createdAt, &issue.Title, &issue.Body, &issue.Open, &metadata.CommentCount)
160 if err != nil {
161 return nil, err
162 }
163
164 createdTime, err := time.Parse(time.RFC3339, createdAt)
165 if err != nil {
166 return nil, err
167 }
168 issue.Created = createdTime
169 issue.Metadata = &metadata
170
171 issues = append(issues, issue)
172 }
173
174 if err := rows.Err(); err != nil {
175 return nil, err
176 }
177
178 return issues, nil
179}
180
181// timeframe here is directly passed into the sql query filter, and any
182// timeframe in the past should be negative; e.g.: "-3 months"
183func GetIssuesByOwnerDid(e Execer, ownerDid string, timeframe string) ([]Issue, error) {
184 var issues []Issue
185
186 rows, err := e.Query(
187 `select
188 i.id,
189 i.owner_did,
190 i.repo_at,
191 i.issue_id,
192 i.created,
193 i.title,
194 i.body,
195 i.open,
196 r.did,
197 r.name,
198 r.knot,
199 r.rkey,
200 r.created
201 from
202 issues i
203 join
204 repos r on i.repo_at = r.at_uri
205 where
206 i.owner_did = ? and i.created >= date ('now', ?)
207 order by
208 i.created desc`,
209 ownerDid, timeframe)
210 if err != nil {
211 return nil, err
212 }
213 defer rows.Close()
214
215 for rows.Next() {
216 var issue Issue
217 var issueCreatedAt, repoCreatedAt string
218 var repo Repo
219 err := rows.Scan(
220 &issue.ID,
221 &issue.OwnerDid,
222 &issue.RepoAt,
223 &issue.IssueId,
224 &issueCreatedAt,
225 &issue.Title,
226 &issue.Body,
227 &issue.Open,
228 &repo.Did,
229 &repo.Name,
230 &repo.Knot,
231 &repo.Rkey,
232 &repoCreatedAt,
233 )
234 if err != nil {
235 return nil, err
236 }
237
238 issueCreatedTime, err := time.Parse(time.RFC3339, issueCreatedAt)
239 if err != nil {
240 return nil, err
241 }
242 issue.Created = issueCreatedTime
243
244 repoCreatedTime, err := time.Parse(time.RFC3339, repoCreatedAt)
245 if err != nil {
246 return nil, err
247 }
248 repo.Created = repoCreatedTime
249
250 issue.Metadata = &IssueMetadata{
251 Repo: &repo,
252 }
253
254 issues = append(issues, issue)
255 }
256
257 if err := rows.Err(); err != nil {
258 return nil, err
259 }
260
261 return issues, nil
262}
263
264func GetIssue(e Execer, repoAt syntax.ATURI, issueId int) (*Issue, error) {
265 query := `select id, owner_did, created, title, body, open from issues where repo_at = ? and issue_id = ?`
266 row := e.QueryRow(query, repoAt, issueId)
267
268 var issue Issue
269 var createdAt string
270 err := row.Scan(&issue.ID, &issue.OwnerDid, &createdAt, &issue.Title, &issue.Body, &issue.Open)
271 if err != nil {
272 return nil, err
273 }
274
275 createdTime, err := time.Parse(time.RFC3339, createdAt)
276 if err != nil {
277 return nil, err
278 }
279 issue.Created = createdTime
280
281 return &issue, nil
282}
283
284func GetIssueWithComments(e Execer, repoAt syntax.ATURI, issueId int) (*Issue, []Comment, error) {
285 query := `select id, owner_did, issue_id, created, title, body, open, issue_at from issues where repo_at = ? and issue_id = ?`
286 row := e.QueryRow(query, repoAt, issueId)
287
288 var issue Issue
289 var createdAt string
290 err := row.Scan(&issue.ID, &issue.OwnerDid, &issue.IssueId, &createdAt, &issue.Title, &issue.Body, &issue.Open, &issue.IssueAt)
291 if err != nil {
292 return nil, nil, err
293 }
294
295 createdTime, err := time.Parse(time.RFC3339, createdAt)
296 if err != nil {
297 return nil, nil, err
298 }
299 issue.Created = createdTime
300
301 comments, err := GetComments(e, repoAt, issueId)
302 if err != nil {
303 return nil, nil, err
304 }
305
306 return &issue, comments, nil
307}
308
309func NewIssueComment(e Execer, comment *Comment) error {
310 query := `insert into comments (owner_did, repo_at, rkey, issue_id, comment_id, body) values (?, ?, ?, ?, ?, ?)`
311 _, err := e.Exec(
312 query,
313 comment.OwnerDid,
314 comment.RepoAt,
315 comment.Rkey,
316 comment.Issue,
317 comment.CommentId,
318 comment.Body,
319 )
320 return err
321}
322
323func GetComments(e Execer, repoAt syntax.ATURI, issueId int) ([]Comment, error) {
324 var comments []Comment
325
326 rows, err := e.Query(`
327 select
328 owner_did,
329 issue_id,
330 comment_id,
331 rkey,
332 body,
333 created,
334 edited,
335 deleted
336 from
337 comments
338 where
339 repo_at = ? and issue_id = ?
340 order by
341 created asc`,
342 repoAt,
343 issueId,
344 )
345 if err == sql.ErrNoRows {
346 return []Comment{}, nil
347 }
348 if err != nil {
349 return nil, err
350 }
351 defer rows.Close()
352
353 for rows.Next() {
354 var comment Comment
355 var createdAt string
356 var deletedAt, editedAt, rkey sql.NullString
357 err := rows.Scan(&comment.OwnerDid, &comment.Issue, &comment.CommentId, &rkey, &comment.Body, &createdAt, &editedAt, &deletedAt)
358 if err != nil {
359 return nil, err
360 }
361
362 createdAtTime, err := time.Parse(time.RFC3339, createdAt)
363 if err != nil {
364 return nil, err
365 }
366 comment.Created = &createdAtTime
367
368 if deletedAt.Valid {
369 deletedTime, err := time.Parse(time.RFC3339, deletedAt.String)
370 if err != nil {
371 return nil, err
372 }
373 comment.Deleted = &deletedTime
374 }
375
376 if editedAt.Valid {
377 editedTime, err := time.Parse(time.RFC3339, editedAt.String)
378 if err != nil {
379 return nil, err
380 }
381 comment.Edited = &editedTime
382 }
383
384 if rkey.Valid {
385 comment.Rkey = rkey.String
386 }
387
388 comments = append(comments, comment)
389 }
390
391 if err := rows.Err(); err != nil {
392 return nil, err
393 }
394
395 return comments, nil
396}
397
398func GetComment(e Execer, repoAt syntax.ATURI, issueId, commentId int) (*Comment, error) {
399 query := `
400 select
401 owner_did, body, rkey, created, deleted, edited
402 from
403 comments where repo_at = ? and issue_id = ? and comment_id = ?
404 `
405 row := e.QueryRow(query, repoAt, issueId, commentId)
406
407 var comment Comment
408 var createdAt string
409 var deletedAt, editedAt, rkey sql.NullString
410 err := row.Scan(&comment.OwnerDid, &comment.Body, &rkey, &createdAt, &deletedAt, &editedAt)
411 if err != nil {
412 return nil, err
413 }
414
415 createdTime, err := time.Parse(time.RFC3339, createdAt)
416 if err != nil {
417 return nil, err
418 }
419 comment.Created = &createdTime
420
421 if deletedAt.Valid {
422 deletedTime, err := time.Parse(time.RFC3339, deletedAt.String)
423 if err != nil {
424 return nil, err
425 }
426 comment.Deleted = &deletedTime
427 }
428
429 if editedAt.Valid {
430 editedTime, err := time.Parse(time.RFC3339, editedAt.String)
431 if err != nil {
432 return nil, err
433 }
434 comment.Edited = &editedTime
435 }
436
437 if rkey.Valid {
438 comment.Rkey = rkey.String
439 }
440
441 comment.RepoAt = repoAt
442 comment.Issue = issueId
443 comment.CommentId = commentId
444
445 return &comment, nil
446}
447
448func EditComment(e Execer, repoAt syntax.ATURI, issueId, commentId int, newBody string) error {
449 _, err := e.Exec(
450 `
451 update comments
452 set body = ?,
453 edited = strftime('%Y-%m-%dT%H:%M:%SZ', 'now')
454 where repo_at = ? and issue_id = ? and comment_id = ?
455 `, newBody, repoAt, issueId, commentId)
456 return err
457}
458
459func DeleteComment(e Execer, repoAt syntax.ATURI, issueId, commentId int) error {
460 _, err := e.Exec(
461 `
462 update comments
463 set body = "",
464 deleted = strftime('%Y-%m-%dT%H:%M:%SZ', 'now')
465 where repo_at = ? and issue_id = ? and comment_id = ?
466 `, repoAt, issueId, commentId)
467 return err
468}
469
470func CloseIssue(e Execer, repoAt syntax.ATURI, issueId int) error {
471 _, err := e.Exec(`update issues set open = 0 where repo_at = ? and issue_id = ?`, repoAt, issueId)
472 return err
473}
474
475func ReopenIssue(e Execer, repoAt syntax.ATURI, issueId int) error {
476 _, err := e.Exec(`update issues set open = 1 where repo_at = ? and issue_id = ?`, repoAt, issueId)
477 return err
478}
479
480type IssueCount struct {
481 Open int
482 Closed int
483}
484
485func GetIssueCount(e Execer, repoAt syntax.ATURI) (IssueCount, error) {
486 row := e.QueryRow(`
487 select
488 count(case when open = 1 then 1 end) as open_count,
489 count(case when open = 0 then 1 end) as closed_count
490 from issues
491 where repo_at = ?`,
492 repoAt,
493 )
494
495 var count IssueCount
496 if err := row.Scan(&count.Open, &count.Closed); err != nil {
497 return IssueCount{0, 0}, err
498 }
499
500 return count, nil
501}