+267
-235
appview/db/issues.go
+267
-235
appview/db/issues.go
···
240
240
return nil
241
241
}
242
242
243
-
func GetIssueAt(e Execer, repoAt syntax.ATURI, issueId int) (string, error) {
244
-
var issueAt string
245
-
err := e.QueryRow(`select issue_at from issues where repo_at = ? and issue_id = ?`, repoAt, issueId).Scan(&issueAt)
246
-
return issueAt, err
247
-
}
243
+
func GetIssuesPaginated(e Execer, page pagination.Page, filters ...filter) ([]Issue, error) {
244
+
issueMap := make(map[string]*Issue) // at-uri -> issue
248
245
249
-
func GetIssueOwnerDid(e Execer, repoAt syntax.ATURI, issueId int) (string, error) {
250
-
var ownerDid string
251
-
err := e.QueryRow(`select owner_did from issues where repo_at = ? and issue_id = ?`, repoAt, issueId).Scan(&ownerDid)
252
-
return ownerDid, err
253
-
}
246
+
var conditions []string
247
+
var args []any
254
248
255
-
func GetIssuesPaginated(e Execer, repoAt syntax.ATURI, isOpen bool, page pagination.Page) ([]Issue, error) {
256
-
var issues []Issue
257
-
openValue := 0
258
-
if isOpen {
259
-
openValue = 1
249
+
for _, filter := range filters {
250
+
conditions = append(conditions, filter.Condition())
251
+
args = append(args, filter.Arg()...)
260
252
}
261
253
262
-
rows, err := e.Query(
254
+
whereClause := ""
255
+
if conditions != nil {
256
+
whereClause = " where " + strings.Join(conditions, " and ")
257
+
}
258
+
259
+
pLower := FilterGte("row_num", page.Offset+1)
260
+
pUpper := FilterLte("row_num", page.Offset+page.Limit)
261
+
262
+
args = append(args, pLower.Arg()...)
263
+
args = append(args, pUpper.Arg()...)
264
+
pagination := " where " + pLower.Condition() + " and " + pUpper.Condition()
265
+
266
+
query := fmt.Sprintf(
263
267
`
264
-
with numbered_issue as (
268
+
select * from (
265
269
select
266
-
i.id,
267
-
i.owner_did,
268
-
i.rkey,
269
-
i.issue_id,
270
-
i.created,
271
-
i.title,
272
-
i.body,
273
-
i.open,
274
-
count(c.id) as comment_count,
275
-
row_number() over (order by i.created desc) as row_num
270
+
id,
271
+
did,
272
+
rkey,
273
+
repo_at,
274
+
issue_id,
275
+
title,
276
+
body,
277
+
open,
278
+
created,
279
+
edited,
280
+
deleted,
281
+
row_number() over (order by created desc) as row_num
276
282
from
277
-
issues i
278
-
left join
279
-
comments c on i.repo_at = c.repo_at and i.issue_id = c.issue_id
280
-
where
281
-
i.repo_at = ? and i.open = ?
282
-
group by
283
-
i.id, i.owner_did, i.issue_id, i.created, i.title, i.body, i.open
284
-
)
285
-
select
286
-
id,
287
-
owner_did,
288
-
rkey,
289
-
issue_id,
290
-
created,
291
-
title,
292
-
body,
293
-
open,
294
-
comment_count
295
-
from
296
-
numbered_issue
297
-
where
298
-
row_num between ? and ?`,
299
-
repoAt, openValue, page.Offset+1, page.Offset+page.Limit)
283
+
issues
284
+
%s
285
+
) ranked_issues
286
+
%s
287
+
`,
288
+
whereClause,
289
+
pagination,
290
+
)
291
+
292
+
rows, err := e.Query(query, args...)
300
293
if err != nil {
301
-
return nil, err
294
+
return nil, fmt.Errorf("failed to query issues table: %w", err)
302
295
}
303
296
defer rows.Close()
304
297
305
298
for rows.Next() {
306
299
var issue Issue
307
300
var createdAt string
308
-
var metadata IssueMetadata
309
-
err := rows.Scan(&issue.ID, &issue.OwnerDid, &issue.Rkey, &issue.IssueId, &createdAt, &issue.Title, &issue.Body, &issue.Open, &metadata.CommentCount)
301
+
var editedAt, deletedAt sql.Null[string]
302
+
var rowNum int64
303
+
err := rows.Scan(
304
+
&issue.Id,
305
+
&issue.Did,
306
+
&issue.Rkey,
307
+
&issue.RepoAt,
308
+
&issue.IssueId,
309
+
&issue.Title,
310
+
&issue.Body,
311
+
&issue.Open,
312
+
&createdAt,
313
+
&editedAt,
314
+
&deletedAt,
315
+
&rowNum,
316
+
)
310
317
if err != nil {
311
-
return nil, err
318
+
return nil, fmt.Errorf("failed to scan issue: %w", err)
319
+
}
320
+
321
+
if t, err := time.Parse(time.RFC3339, createdAt); err == nil {
322
+
issue.Created = t
323
+
}
324
+
325
+
if editedAt.Valid {
326
+
if t, err := time.Parse(time.RFC3339, editedAt.V); err == nil {
327
+
issue.Edited = &t
328
+
}
312
329
}
313
330
314
-
createdTime, err := time.Parse(time.RFC3339, createdAt)
315
-
if err != nil {
316
-
return nil, err
331
+
if deletedAt.Valid {
332
+
if t, err := time.Parse(time.RFC3339, deletedAt.V); err == nil {
333
+
issue.Deleted = &t
334
+
}
317
335
}
318
-
issue.Created = createdTime
319
-
issue.Metadata = &metadata
336
+
337
+
atUri := issue.AtUri().String()
338
+
issueMap[atUri] = &issue
339
+
}
340
+
341
+
// collect reverse repos
342
+
repoAts := make([]string, 0, len(issueMap)) // or just []string{}
343
+
for _, issue := range issueMap {
344
+
repoAts = append(repoAts, string(issue.RepoAt))
345
+
}
346
+
347
+
repos, err := GetRepos(e, 0, FilterIn("at_uri", repoAts))
348
+
if err != nil {
349
+
return nil, fmt.Errorf("failed to build repo mappings: %w", err)
350
+
}
351
+
352
+
repoMap := make(map[string]*Repo)
353
+
for i := range repos {
354
+
repoMap[string(repos[i].RepoAt())] = &repos[i]
355
+
}
356
+
357
+
for issueAt := range issueMap {
358
+
i := issueMap[issueAt]
359
+
r := repoMap[string(i.RepoAt)]
360
+
i.Repo = r
361
+
}
320
362
321
-
issues = append(issues, issue)
363
+
// collect comments
364
+
issueAts := slices.Collect(maps.Keys(issueMap))
365
+
comments, err := GetIssueComments(e, FilterIn("issue_at", issueAts))
366
+
if err != nil {
367
+
return nil, fmt.Errorf("failed to query comments: %w", err)
322
368
}
323
369
324
-
if err := rows.Err(); err != nil {
325
-
return nil, err
370
+
for i := range comments {
371
+
issueAt := comments[i].IssueAt
372
+
if issue, ok := issueMap[issueAt]; ok {
373
+
issue.Comments = append(issue.Comments, comments[i])
374
+
}
375
+
}
376
+
377
+
var issues []Issue
378
+
for _, i := range issueMap {
379
+
issues = append(issues, *i)
326
380
}
381
+
382
+
sort.Slice(issues, func(i, j int) bool {
383
+
return issues[i].Created.After(issues[j].Created)
384
+
})
327
385
328
386
return issues, nil
329
387
}
···
375
433
var issue Issue
376
434
var issueCreatedAt string
377
435
err := rows.Scan(
378
-
&issue.ID,
379
-
&issue.OwnerDid,
436
+
&issue.Id,
437
+
&issue.Did,
380
438
&issue.RepoAt,
381
439
&issue.IssueId,
382
440
&issueCreatedAt,
···
405
463
}
406
464
407
465
func GetIssues(e Execer, filters ...filter) ([]Issue, error) {
408
-
return GetIssuesWithLimit(e, 0, filters...)
466
+
return GetIssuesPaginated(e, pagination.FirstPage(), filters...)
409
467
}
410
468
411
469
// timeframe here is directly passed into the sql query filter, and any
···
448
506
var issueCreatedAt, repoCreatedAt string
449
507
var repo Repo
450
508
err := rows.Scan(
451
-
&issue.ID,
452
-
&issue.OwnerDid,
509
+
&issue.Id,
510
+
&issue.Did,
453
511
&issue.Rkey,
454
512
&issue.RepoAt,
455
513
&issue.IssueId,
···
479
537
}
480
538
repo.Created = repoCreatedTime
481
539
482
-
issue.Metadata = &IssueMetadata{
483
-
Repo: &repo,
484
-
}
485
-
486
540
issues = append(issues, issue)
487
541
}
488
542
···
499
553
500
554
var issue Issue
501
555
var createdAt string
502
-
err := row.Scan(&issue.ID, &issue.OwnerDid, &issue.Rkey, &createdAt, &issue.Title, &issue.Body, &issue.Open)
556
+
err := row.Scan(&issue.Id, &issue.Did, &issue.Rkey, &createdAt, &issue.Title, &issue.Body, &issue.Open)
503
557
if err != nil {
504
558
return nil, err
505
559
}
···
513
567
return &issue, nil
514
568
}
515
569
516
-
func GetIssueWithComments(e Execer, repoAt syntax.ATURI, issueId int) (*Issue, []Comment, error) {
517
-
query := `select id, owner_did, rkey, issue_id, created, title, body, open from issues where repo_at = ? and issue_id = ?`
518
-
row := e.QueryRow(query, repoAt, issueId)
519
-
520
-
var issue Issue
521
-
var createdAt string
522
-
err := row.Scan(&issue.ID, &issue.OwnerDid, &issue.Rkey, &issue.IssueId, &createdAt, &issue.Title, &issue.Body, &issue.Open)
570
+
func AddIssueComment(e Execer, c IssueComment) (int64, error) {
571
+
result, err := e.Exec(
572
+
`insert into issue_comments (
573
+
did,
574
+
rkey,
575
+
issue_at,
576
+
body,
577
+
reply_to,
578
+
created,
579
+
edited
580
+
)
581
+
values (?, ?, ?, ?, ?, ?, null)
582
+
on conflict(did, rkey) do update set
583
+
issue_at = excluded.issue_at,
584
+
body = excluded.body,
585
+
edited = case
586
+
when
587
+
issue_comments.issue_at != excluded.issue_at
588
+
or issue_comments.body != excluded.body
589
+
or issue_comments.reply_to != excluded.reply_to
590
+
then ?
591
+
else issue_comments.edited
592
+
end`,
593
+
c.Did,
594
+
c.Rkey,
595
+
c.IssueAt,
596
+
c.Body,
597
+
c.ReplyTo,
598
+
c.Created.Format(time.RFC3339),
599
+
time.Now().Format(time.RFC3339),
600
+
)
523
601
if err != nil {
524
-
return nil, nil, err
602
+
return 0, err
525
603
}
526
604
527
-
createdTime, err := time.Parse(time.RFC3339, createdAt)
605
+
id, err := result.LastInsertId()
528
606
if err != nil {
529
-
return nil, nil, err
607
+
return 0, err
530
608
}
531
-
issue.Created = createdTime
609
+
610
+
return id, nil
611
+
}
532
612
533
-
comments, err := GetComments(e, repoAt, issueId)
534
-
if err != nil {
535
-
return nil, nil, err
613
+
func DeleteIssueComments(e Execer, filters ...filter) error {
614
+
var conditions []string
615
+
var args []any
616
+
for _, filter := range filters {
617
+
conditions = append(conditions, filter.Condition())
618
+
args = append(args, filter.Arg()...)
536
619
}
537
620
538
-
return &issue, comments, nil
539
-
}
621
+
whereClause := ""
622
+
if conditions != nil {
623
+
whereClause = " where " + strings.Join(conditions, " and ")
624
+
}
540
625
541
-
func NewIssueComment(e Execer, comment *Comment) error {
542
-
query := `insert into comments (owner_did, repo_at, rkey, issue_id, comment_id, body) values (?, ?, ?, ?, ?, ?)`
543
-
_, err := e.Exec(
544
-
query,
545
-
comment.OwnerDid,
546
-
comment.RepoAt,
547
-
comment.Rkey,
548
-
comment.Issue,
549
-
comment.CommentId,
550
-
comment.Body,
551
-
)
626
+
query := fmt.Sprintf(`update issue_comments set body = "", deleted = strftime('%%Y-%%m-%%dT%%H:%%M:%%SZ', 'now') %s`, whereClause)
627
+
628
+
_, err := e.Exec(query, args...)
552
629
return err
553
630
}
554
631
555
-
func GetComments(e Execer, repoAt syntax.ATURI, issueId int) ([]Comment, error) {
556
-
var comments []Comment
632
+
func GetIssueComments(e Execer, filters ...filter) ([]IssueComment, error) {
633
+
var comments []IssueComment
634
+
635
+
var conditions []string
636
+
var args []any
637
+
for _, filter := range filters {
638
+
conditions = append(conditions, filter.Condition())
639
+
args = append(args, filter.Arg()...)
640
+
}
557
641
558
-
rows, err := e.Query(`
642
+
whereClause := ""
643
+
if conditions != nil {
644
+
whereClause = " where " + strings.Join(conditions, " and ")
645
+
}
646
+
647
+
query := fmt.Sprintf(`
559
648
select
560
-
owner_did,
561
-
issue_id,
562
-
comment_id,
649
+
id,
650
+
did,
563
651
rkey,
652
+
issue_at,
653
+
reply_to,
564
654
body,
565
655
created,
566
656
edited,
567
657
deleted
568
658
from
569
-
comments
570
-
where
571
-
repo_at = ? and issue_id = ?
572
-
order by
573
-
created asc`,
574
-
repoAt,
575
-
issueId,
576
-
)
577
-
if err == sql.ErrNoRows {
578
-
return []Comment{}, nil
579
-
}
659
+
issue_comments
660
+
%s
661
+
`, whereClause)
662
+
663
+
rows, err := e.Query(query, args...)
580
664
if err != nil {
581
665
return nil, err
582
666
}
583
-
defer rows.Close()
584
667
585
668
for rows.Next() {
586
-
var comment Comment
587
-
var createdAt string
588
-
var deletedAt, editedAt, rkey sql.NullString
589
-
err := rows.Scan(&comment.OwnerDid, &comment.Issue, &comment.CommentId, &rkey, &comment.Body, &createdAt, &editedAt, &deletedAt)
669
+
var comment IssueComment
670
+
var created string
671
+
var rkey, edited, deleted, replyTo sql.Null[string]
672
+
err := rows.Scan(
673
+
&comment.Id,
674
+
&comment.Did,
675
+
&rkey,
676
+
&comment.IssueAt,
677
+
&replyTo,
678
+
&comment.Body,
679
+
&created,
680
+
&edited,
681
+
&deleted,
682
+
)
590
683
if err != nil {
591
684
return nil, err
592
685
}
593
686
594
-
createdAtTime, err := time.Parse(time.RFC3339, createdAt)
595
-
if err != nil {
596
-
return nil, err
687
+
// this is a remnant from old times, newer comments always have rkey
688
+
if rkey.Valid {
689
+
comment.Rkey = rkey.V
597
690
}
598
-
comment.Created = &createdAtTime
691
+
692
+
if t, err := time.Parse(time.RFC3339, created); err == nil {
693
+
comment.Created = t
694
+
}
599
695
600
-
if deletedAt.Valid {
601
-
deletedTime, err := time.Parse(time.RFC3339, deletedAt.String)
602
-
if err != nil {
603
-
return nil, err
696
+
if edited.Valid {
697
+
if t, err := time.Parse(time.RFC3339, edited.V); err == nil {
698
+
comment.Edited = &t
604
699
}
605
-
comment.Deleted = &deletedTime
606
700
}
607
701
608
-
if editedAt.Valid {
609
-
editedTime, err := time.Parse(time.RFC3339, editedAt.String)
610
-
if err != nil {
611
-
return nil, err
702
+
if deleted.Valid {
703
+
if t, err := time.Parse(time.RFC3339, deleted.V); err == nil {
704
+
comment.Deleted = &t
612
705
}
613
-
comment.Edited = &editedTime
614
706
}
615
707
616
-
if rkey.Valid {
617
-
comment.Rkey = rkey.String
708
+
if replyTo.Valid {
709
+
comment.ReplyTo = &replyTo.V
618
710
}
619
711
620
712
comments = append(comments, comment)
621
713
}
622
714
623
-
if err := rows.Err(); err != nil {
715
+
if err = rows.Err(); err != nil {
624
716
return nil, err
625
717
}
626
718
627
719
return comments, nil
628
720
}
629
721
630
-
func GetComment(e Execer, repoAt syntax.ATURI, issueId, commentId int) (*Comment, error) {
631
-
query := `
632
-
select
633
-
owner_did, body, rkey, created, deleted, edited
634
-
from
635
-
comments where repo_at = ? and issue_id = ? and comment_id = ?
636
-
`
637
-
row := e.QueryRow(query, repoAt, issueId, commentId)
638
-
639
-
var comment Comment
640
-
var createdAt string
641
-
var deletedAt, editedAt, rkey sql.NullString
642
-
err := row.Scan(&comment.OwnerDid, &comment.Body, &rkey, &createdAt, &deletedAt, &editedAt)
643
-
if err != nil {
644
-
return nil, err
722
+
func DeleteIssues(e Execer, filters ...filter) error {
723
+
var conditions []string
724
+
var args []any
725
+
for _, filter := range filters {
726
+
conditions = append(conditions, filter.Condition())
727
+
args = append(args, filter.Arg()...)
645
728
}
646
729
647
-
createdTime, err := time.Parse(time.RFC3339, createdAt)
648
-
if err != nil {
649
-
return nil, err
730
+
whereClause := ""
731
+
if conditions != nil {
732
+
whereClause = " where " + strings.Join(conditions, " and ")
650
733
}
651
-
comment.Created = &createdTime
652
734
653
-
if deletedAt.Valid {
654
-
deletedTime, err := time.Parse(time.RFC3339, deletedAt.String)
655
-
if err != nil {
656
-
return nil, err
657
-
}
658
-
comment.Deleted = &deletedTime
659
-
}
735
+
query := fmt.Sprintf(`delete from issues %s`, whereClause)
736
+
_, err := e.Exec(query, args...)
737
+
return err
738
+
}
660
739
661
-
if editedAt.Valid {
662
-
editedTime, err := time.Parse(time.RFC3339, editedAt.String)
663
-
if err != nil {
664
-
return nil, err
665
-
}
666
-
comment.Edited = &editedTime
740
+
func CloseIssues(e Execer, filters ...filter) error {
741
+
var conditions []string
742
+
var args []any
743
+
for _, filter := range filters {
744
+
conditions = append(conditions, filter.Condition())
745
+
args = append(args, filter.Arg()...)
667
746
}
668
747
669
-
if rkey.Valid {
670
-
comment.Rkey = rkey.String
748
+
whereClause := ""
749
+
if conditions != nil {
750
+
whereClause = " where " + strings.Join(conditions, " and ")
671
751
}
672
752
673
-
comment.RepoAt = repoAt
674
-
comment.Issue = issueId
675
-
comment.CommentId = commentId
676
-
677
-
return &comment, nil
678
-
}
679
-
680
-
func EditComment(e Execer, repoAt syntax.ATURI, issueId, commentId int, newBody string) error {
681
-
_, err := e.Exec(
682
-
`
683
-
update comments
684
-
set body = ?,
685
-
edited = strftime('%Y-%m-%dT%H:%M:%SZ', 'now')
686
-
where repo_at = ? and issue_id = ? and comment_id = ?
687
-
`, newBody, repoAt, issueId, commentId)
753
+
query := fmt.Sprintf(`update issues set open = 0 %s`, whereClause)
754
+
_, err := e.Exec(query, args...)
688
755
return err
689
756
}
690
757
691
-
func DeleteComment(e Execer, repoAt syntax.ATURI, issueId, commentId int) error {
692
-
_, err := e.Exec(
693
-
`
694
-
update comments
695
-
set body = "",
696
-
deleted = strftime('%Y-%m-%dT%H:%M:%SZ', 'now')
697
-
where repo_at = ? and issue_id = ? and comment_id = ?
698
-
`, repoAt, issueId, commentId)
699
-
return err
700
-
}
701
-
702
-
func UpdateCommentByRkey(e Execer, ownerDid, rkey, newBody string) error {
703
-
_, err := e.Exec(
704
-
`
705
-
update comments
706
-
set body = ?,
707
-
edited = strftime('%Y-%m-%dT%H:%M:%SZ', 'now')
708
-
where owner_did = ? and rkey = ?
709
-
`, newBody, ownerDid, rkey)
710
-
return err
711
-
}
712
-
713
-
func DeleteCommentByRkey(e Execer, ownerDid, rkey string) error {
714
-
_, err := e.Exec(
715
-
`
716
-
update comments
717
-
set body = "",
718
-
deleted = strftime('%Y-%m-%dT%H:%M:%SZ', 'now')
719
-
where owner_did = ? and rkey = ?
720
-
`, ownerDid, rkey)
721
-
return err
722
-
}
758
+
func ReopenIssues(e Execer, filters ...filter) error {
759
+
var conditions []string
760
+
var args []any
761
+
for _, filter := range filters {
762
+
conditions = append(conditions, filter.Condition())
763
+
args = append(args, filter.Arg()...)
764
+
}
723
765
724
-
func UpdateIssueByRkey(e Execer, ownerDid, rkey, title, body string) error {
725
-
_, err := e.Exec(`update issues set title = ?, body = ? where owner_did = ? and rkey = ?`, title, body, ownerDid, rkey)
726
-
return err
727
-
}
766
+
whereClause := ""
767
+
if conditions != nil {
768
+
whereClause = " where " + strings.Join(conditions, " and ")
769
+
}
728
770
729
-
func DeleteIssueByRkey(e Execer, ownerDid, rkey string) error {
730
-
_, err := e.Exec(`delete from issues where owner_did = ? and rkey = ?`, ownerDid, rkey)
731
-
return err
732
-
}
733
-
734
-
func CloseIssue(e Execer, repoAt syntax.ATURI, issueId int) error {
735
-
_, err := e.Exec(`update issues set open = 0 where repo_at = ? and issue_id = ?`, repoAt, issueId)
736
-
return err
737
-
}
738
-
739
-
func ReopenIssue(e Execer, repoAt syntax.ATURI, issueId int) error {
740
-
_, err := e.Exec(`update issues set open = 1 where repo_at = ? and issue_id = ?`, repoAt, issueId)
771
+
query := fmt.Sprintf(`update issues set open = 1 %s`, whereClause)
772
+
_, err := e.Exec(query, args...)
741
773
return err
742
774
}
743
775
+3
-3
appview/state/profile.go
+3
-3
appview/state/profile.go
···
467
467
468
468
func (s *State) addIssueItems(ctx context.Context, feed *feeds.Feed, issues []*db.Issue, author *feeds.Author) error {
469
469
for _, issue := range issues {
470
-
owner, err := s.idResolver.ResolveIdent(ctx, issue.Metadata.Repo.Did)
470
+
owner, err := s.idResolver.ResolveIdent(ctx, issue.Repo.Did)
471
471
if err != nil {
472
472
return err
473
473
}
···
499
499
500
500
func (s *State) createIssueItem(issue *db.Issue, owner *identity.Identity, author *feeds.Author) *feeds.Item {
501
501
return &feeds.Item{
502
-
Title: fmt.Sprintf("%s created issue '%s' in @%s/%s", author.Name, issue.Title, owner.Handle, issue.Metadata.Repo.Name),
503
-
Link: &feeds.Link{Href: fmt.Sprintf("%s/@%s/%s/issues/%d", s.config.Core.AppviewHost, owner.Handle, issue.Metadata.Repo.Name, issue.IssueId), Type: "text/html", Rel: "alternate"},
502
+
Title: fmt.Sprintf("%s created issue '%s' in @%s/%s", author.Name, issue.Title, owner.Handle, issue.Repo.Name),
503
+
Link: &feeds.Link{Href: fmt.Sprintf("%s/@%s/%s/issues/%d", s.config.Core.AppviewHost, owner.Handle, issue.Repo.Name, issue.IssueId), Type: "text/html", Rel: "alternate"},
504
504
Created: issue.Created,
505
505
Author: author,
506
506
}