forked from tangled.org/core
Monorepo for Tangled

appview/db: new schemas for issues and comments

- all Create ops are upserts by default, this means the ingester simply
has to create a new item during ingestion, the db handler will decide
if it is an edit or a create operation
- all ops have been updated to use db.Filter

Signed-off-by: oppiliappan <me@oppi.li>

oppi.li 9d3f49a9 d7569292

verified
Changed files
+113 -65
appview
+113 -65
appview/db/issues.go
··· 3 import ( 4 "database/sql" 5 "fmt" 6 - mathrand "math/rand/v2" 7 "strings" 8 "time" 9 ··· 13 ) 14 15 type Issue struct { 16 - ID int64 17 - RepoAt syntax.ATURI 18 - OwnerDid string 19 - IssueId int 20 - Rkey string 21 - Created time.Time 22 - Title string 23 - Body string 24 - Open bool 25 26 // optionally, populate this when querying for reverse mappings 27 // like comment counts, parent repo etc. 28 - Metadata *IssueMetadata 29 } 30 31 - type IssueMetadata struct { 32 - CommentCount int 33 - Repo *Repo 34 - // labels, assignee etc. 35 } 36 37 - type Comment struct { 38 - OwnerDid string 39 - RepoAt syntax.ATURI 40 - Rkey string 41 - Issue int 42 - CommentId int 43 - Body string 44 - Created *time.Time 45 - Deleted *time.Time 46 - Edited *time.Time 47 } 48 49 - func (i *Issue) AtUri() syntax.ATURI { 50 - return syntax.ATURI(fmt.Sprintf("at://%s/%s/%s", i.OwnerDid, tangled.RepoIssueNSID, i.Rkey)) 51 } 52 53 func IssueFromRecord(did, rkey string, record tangled.RepoIssue) Issue { ··· 62 } 63 64 return Issue{ 65 - RepoAt: syntax.ATURI(record.Repo), 66 - OwnerDid: did, 67 - Rkey: rkey, 68 - Created: created, 69 - Title: record.Title, 70 - Body: body, 71 - Open: true, // new issues are open by default 72 } 73 } 74 75 - func ResolveIssueFromAtUri(e Execer, issueUri syntax.ATURI) (syntax.ATURI, int, error) { 76 - ownerDid := issueUri.Authority().String() 77 - issueRkey := issueUri.RecordKey().String() 78 79 - var repoAt string 80 - var issueId int 81 82 - query := `select repo_at, issue_id from issues where owner_did = ? and rkey = ?` 83 - err := e.QueryRow(query, ownerDid, issueRkey).Scan(&repoAt, &issueId) 84 - if err != nil { 85 - return "", 0, err 86 } 87 88 - return syntax.ATURI(repoAt), issueId, nil 89 } 90 91 - func IssueCommentFromRecord(e Execer, did, rkey string, record tangled.RepoIssueComment) (Comment, error) { 92 created, err := time.Parse(time.RFC3339, record.CreatedAt) 93 if err != nil { 94 created = time.Now() 95 } 96 97 ownerDid := did 98 - if record.Owner != nil { 99 - ownerDid = *record.Owner 100 - } 101 - 102 - issueUri, err := syntax.ParseATURI(record.Issue) 103 - if err != nil { 104 - return Comment{}, err 105 - } 106 107 - repoAt, issueId, err := ResolveIssueFromAtUri(e, issueUri) 108 - if err != nil { 109 - return Comment{}, err 110 } 111 112 - comment := Comment{ 113 - OwnerDid: ownerDid, 114 - RepoAt: repoAt, 115 - Rkey: rkey, 116 - Body: record.Body, 117 - Issue: issueId, 118 - CommentId: mathrand.IntN(1000000), 119 - Created: &created, 120 } 121 122 - return comment, nil 123 } 124 125 func NewIssue(tx *sql.Tx, issue *Issue) error {
··· 3 import ( 4 "database/sql" 5 "fmt" 6 + "maps" 7 + "slices" 8 + "sort" 9 "strings" 10 "time" 11 ··· 15 ) 16 17 type Issue struct { 18 + Id int64 19 + Did string 20 + Rkey string 21 + RepoAt syntax.ATURI 22 + IssueId int 23 + Created time.Time 24 + Edited *time.Time 25 + Deleted *time.Time 26 + Title string 27 + Body string 28 + Open bool 29 30 // optionally, populate this when querying for reverse mappings 31 // like comment counts, parent repo etc. 32 + Comments []IssueComment 33 + Repo *Repo 34 } 35 36 + func (i *Issue) AtUri() syntax.ATURI { 37 + return syntax.ATURI(fmt.Sprintf("at://%s/%s/%s", i.Did, tangled.RepoIssueNSID, i.Rkey)) 38 } 39 40 + func (i *Issue) AsRecord() tangled.RepoIssue { 41 + return tangled.RepoIssue{ 42 + Repo: i.RepoAt.String(), 43 + Title: i.Title, 44 + Body: &i.Body, 45 + CreatedAt: i.Created.Format(time.RFC3339), 46 + } 47 } 48 49 + type CommentListItem struct { 50 + Self *IssueComment 51 + Replies []*IssueComment 52 + } 53 + 54 + func (i *Issue) CommentList() []CommentListItem { 55 + // Create a map to quickly find comments by their aturi 56 + toplevel := make(map[string]*CommentListItem) 57 + var replies []*IssueComment 58 + 59 + // collect top level comments into the map 60 + for _, comment := range i.Comments { 61 + if comment.IsTopLevel() { 62 + toplevel[comment.AtUri().String()] = &CommentListItem{ 63 + Self: &comment, 64 + } 65 + } else { 66 + replies = append(replies, &comment) 67 + } 68 + } 69 + 70 + for _, r := range replies { 71 + parentAt := *r.ReplyTo 72 + if parent, exists := toplevel[parentAt]; exists { 73 + parent.Replies = append(parent.Replies, r) 74 + } 75 + } 76 + 77 + var listing []CommentListItem 78 + for _, v := range toplevel { 79 + listing = append(listing, *v) 80 + } 81 + 82 + // sort everything 83 + sortFunc := func(a, b *IssueComment) bool { 84 + return a.Created.Before(b.Created) 85 + } 86 + sort.Slice(listing, func(i, j int) bool { 87 + return sortFunc(listing[i].Self, listing[j].Self) 88 + }) 89 + for _, r := range listing { 90 + sort.Slice(r.Replies, func(i, j int) bool { 91 + return sortFunc(r.Replies[i], r.Replies[j]) 92 + }) 93 + } 94 + 95 + return listing 96 } 97 98 func IssueFromRecord(did, rkey string, record tangled.RepoIssue) Issue { ··· 107 } 108 109 return Issue{ 110 + RepoAt: syntax.ATURI(record.Repo), 111 + Did: did, 112 + Rkey: rkey, 113 + Created: created, 114 + Title: record.Title, 115 + Body: body, 116 + Open: true, // new issues are open by default 117 } 118 } 119 120 + type IssueComment struct { 121 + Id int64 122 + Did string 123 + Rkey string 124 + IssueAt string 125 + ReplyTo *string 126 + Body string 127 + Created time.Time 128 + Edited *time.Time 129 + Deleted *time.Time 130 + } 131 132 + func (i *IssueComment) AtUri() syntax.ATURI { 133 + return syntax.ATURI(fmt.Sprintf("at://%s/%s/%s", i.Did, tangled.RepoIssueCommentNSID, i.Rkey)) 134 + } 135 136 + func (i *IssueComment) AsRecord() tangled.RepoIssueComment { 137 + return tangled.RepoIssueComment{ 138 + Body: i.Body, 139 + Issue: i.IssueAt, 140 + CreatedAt: i.Created.Format(time.RFC3339), 141 + ReplyTo: i.ReplyTo, 142 } 143 + } 144 145 + func (i *IssueComment) IsTopLevel() bool { 146 + return i.ReplyTo == nil 147 } 148 149 + func IssueCommentFromRecord(e Execer, did, rkey string, record tangled.RepoIssueComment) (*IssueComment, error) { 150 created, err := time.Parse(time.RFC3339, record.CreatedAt) 151 if err != nil { 152 created = time.Now() 153 } 154 155 ownerDid := did 156 157 + if _, err = syntax.ParseATURI(record.Issue); err != nil { 158 + return nil, err 159 } 160 161 + comment := IssueComment{ 162 + Did: ownerDid, 163 + Rkey: rkey, 164 + Body: record.Body, 165 + IssueAt: record.Issue, 166 + ReplyTo: record.ReplyTo, 167 + Created: created, 168 } 169 170 + return &comment, nil 171 } 172 173 func NewIssue(tx *sql.Tx, issue *Issue) error {