this repo has no description

appview/db: rework issue and comment CRUD ops

- 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 1268bf50 16cdd10d

verified
+429 -324
+426 -321
appview/db/issues.go
··· 3 3 import ( 4 4 "database/sql" 5 5 "fmt" 6 - mathrand "math/rand/v2" 6 + "maps" 7 + "slices" 8 + "sort" 7 9 "strings" 8 10 "time" 9 11 ··· 13 15 ) 14 16 15 17 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 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 25 29 26 30 // optionally, populate this when querying for reverse mappings 27 31 // like comment counts, parent repo etc. 28 - Metadata *IssueMetadata 32 + Comments []IssueComment 33 + Repo *Repo 29 34 } 30 35 31 - type IssueMetadata struct { 32 - CommentCount int 33 - Repo *Repo 34 - // labels, assignee etc. 36 + func (i *Issue) AtUri() syntax.ATURI { 37 + return syntax.ATURI(fmt.Sprintf("at://%s/%s/%s", i.Did, tangled.RepoIssueNSID, i.Rkey)) 35 38 } 36 39 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 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 47 } 48 48 49 - func (i *Issue) AtUri() syntax.ATURI { 50 - return syntax.ATURI(fmt.Sprintf("at://%s/%s/%s", i.OwnerDid, tangled.RepoIssueNSID, i.Rkey)) 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 51 96 } 52 97 53 98 func IssueFromRecord(did, rkey string, record tangled.RepoIssue) Issue { ··· 62 107 } 63 108 64 109 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 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 72 117 } 73 118 } 74 119 75 - func ResolveIssueFromAtUri(e Execer, issueUri syntax.ATURI) (syntax.ATURI, int, error) { 76 - ownerDid := issueUri.Authority().String() 77 - issueRkey := issueUri.RecordKey().String() 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 + } 78 131 79 - var repoAt string 80 - var issueId int 132 + func (i *IssueComment) AtUri() syntax.ATURI { 133 + return syntax.ATURI(fmt.Sprintf("at://%s/%s/%s", i.Did, tangled.RepoIssueCommentNSID, i.Rkey)) 134 + } 81 135 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 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, 86 142 } 143 + } 87 144 88 - return syntax.ATURI(repoAt), issueId, nil 145 + func (i *IssueComment) IsTopLevel() bool { 146 + return i.ReplyTo == nil 89 147 } 90 148 91 - func IssueCommentFromRecord(e Execer, did, rkey string, record tangled.RepoIssueComment) (Comment, error) { 149 + func IssueCommentFromRecord(e Execer, did, rkey string, record tangled.RepoIssueComment) (*IssueComment, error) { 92 150 created, err := time.Parse(time.RFC3339, record.CreatedAt) 93 151 if err != nil { 94 152 created = time.Now() 95 153 } 96 154 97 155 ownerDid := did 98 - if record.Owner != nil { 99 - ownerDid = *record.Owner 100 - } 101 156 102 - issueUri, err := syntax.ParseATURI(record.Issue) 103 - if err != nil { 104 - return Comment{}, err 157 + if _, err = syntax.ParseATURI(record.Issue); err != nil { 158 + return nil, err 105 159 } 106 160 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, 161 + comment := IssueComment{ 162 + Did: ownerDid, 163 + Rkey: rkey, 164 + Body: record.Body, 165 + IssueAt: record.Issue, 166 + ReplyTo: record.ReplyTo, 167 + Created: created, 120 168 } 121 169 122 - return comment, nil 170 + return &comment, nil 123 171 } 124 172 125 173 func NewIssue(tx *sql.Tx, issue *Issue) error { 126 - defer tx.Rollback() 127 - 174 + // ensure sequence exists 128 175 _, err := tx.Exec(` 129 176 insert or ignore into repo_issue_seqs (repo_at, next_issue_id) 130 177 values (?, 1) 131 - `, issue.RepoAt) 178 + `, issue.RepoAt) 132 179 if err != nil { 133 180 return err 134 181 } 135 182 136 - var nextId int 183 + // check if issue already exists 184 + var existingRowId, existingIssueId sql.NullInt64 137 185 err = tx.QueryRow(` 138 - update repo_issue_seqs 139 - set next_issue_id = next_issue_id + 1 140 - where repo_at = ? 141 - returning next_issue_id - 1 142 - `, issue.RepoAt).Scan(&nextId) 143 - if err != nil { 144 - return err 145 - } 186 + select rowid, issue_id from issues 187 + where did = ? and rkey = ? 188 + `, issue.Did, issue.Rkey).Scan(&existingRowId, &existingIssueId) 146 189 147 - issue.IssueId = nextId 190 + switch { 191 + case err == sql.ErrNoRows: 192 + return createNewIssue(tx, issue) 148 193 149 - res, err := tx.Exec(` 150 - insert into issues (repo_at, owner_did, rkey, issue_at, issue_id, title, body) 151 - values (?, ?, ?, ?, ?, ?, ?) 152 - `, issue.RepoAt, issue.OwnerDid, issue.Rkey, issue.AtUri(), issue.IssueId, issue.Title, issue.Body) 153 - if err != nil { 194 + case err != nil: 154 195 return err 196 + 197 + default: 198 + // Case 3: Issue exists - update it 199 + return updateIssue(tx, issue, existingRowId.Int64, int(existingIssueId.Int64)) 155 200 } 201 + } 156 202 157 - lastID, err := res.LastInsertId() 203 + func createNewIssue(tx *sql.Tx, issue *Issue) error { 204 + // get next issue_id 205 + var newIssueId int 206 + err := tx.QueryRow(` 207 + update repo_issue_seqs 208 + set next_issue_id = next_issue_id + 1 209 + where repo_at = ? 210 + returning next_issue_id - 1 211 + `, issue.RepoAt).Scan(&newIssueId) 158 212 if err != nil { 159 213 return err 160 214 } 161 - issue.ID = lastID 215 + 216 + // insert new issue 217 + row := tx.QueryRow(` 218 + insert into issues (repo_at, did, rkey, issue_id, title, body) 219 + values (?, ?, ?, ?, ?, ?) 220 + returning rowid, issue_id 221 + `, issue.RepoAt, issue.Did, issue.Rkey, newIssueId, issue.Title, issue.Body) 222 + 223 + return row.Scan(&issue.Id, &issue.IssueId) 224 + } 162 225 163 - if err := tx.Commit(); err != nil { 226 + func updateIssue(tx *sql.Tx, issue *Issue, existingRowId int64, existingIssueId int) error { 227 + // update existing issue 228 + _, err := tx.Exec(` 229 + update issues 230 + set title = ?, body = ? 231 + where did = ? and rkey = ? 232 + `, issue.Title, issue.Body, issue.Did, issue.Rkey) 233 + if err != nil { 164 234 return err 165 235 } 166 236 237 + // set the values from existing record 238 + issue.Id = existingRowId 239 + issue.IssueId = existingIssueId 167 240 return nil 168 241 } 169 242 170 - func GetIssueAt(e Execer, repoAt syntax.ATURI, issueId int) (string, error) { 171 - var issueAt string 172 - err := e.QueryRow(`select issue_at from issues where repo_at = ? and issue_id = ?`, repoAt, issueId).Scan(&issueAt) 173 - return issueAt, err 174 - } 243 + func GetIssuesPaginated(e Execer, page pagination.Page, filters ...filter) ([]Issue, error) { 244 + issueMap := make(map[string]*Issue) // at-uri -> issue 175 245 176 - func GetIssueOwnerDid(e Execer, repoAt syntax.ATURI, issueId int) (string, error) { 177 - var ownerDid string 178 - err := e.QueryRow(`select owner_did from issues where repo_at = ? and issue_id = ?`, repoAt, issueId).Scan(&ownerDid) 179 - return ownerDid, err 180 - } 246 + var conditions []string 247 + var args []any 248 + 249 + for _, filter := range filters { 250 + conditions = append(conditions, filter.Condition()) 251 + args = append(args, filter.Arg()...) 252 + } 181 253 182 - func GetIssuesPaginated(e Execer, repoAt syntax.ATURI, isOpen bool, page pagination.Page) ([]Issue, error) { 183 - var issues []Issue 184 - openValue := 0 185 - if isOpen { 186 - openValue = 1 254 + whereClause := "" 255 + if conditions != nil { 256 + whereClause = " where " + strings.Join(conditions, " and ") 187 257 } 188 258 189 - rows, err := e.Query( 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( 190 267 ` 191 - with numbered_issue as ( 268 + select * from ( 192 269 select 193 - i.id, 194 - i.owner_did, 195 - i.rkey, 196 - i.issue_id, 197 - i.created, 198 - i.title, 199 - i.body, 200 - i.open, 201 - count(c.id) as comment_count, 202 - 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 203 282 from 204 - issues i 205 - left join 206 - comments c on i.repo_at = c.repo_at and i.issue_id = c.issue_id 207 - where 208 - i.repo_at = ? and i.open = ? 209 - group by 210 - i.id, i.owner_did, i.issue_id, i.created, i.title, i.body, i.open 211 - ) 212 - select 213 - id, 214 - owner_did, 215 - rkey, 216 - issue_id, 217 - created, 218 - title, 219 - body, 220 - open, 221 - comment_count 222 - from 223 - numbered_issue 224 - where 225 - row_num between ? and ?`, 226 - 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...) 227 293 if err != nil { 228 - return nil, err 294 + return nil, fmt.Errorf("failed to query issues table: %w", err) 229 295 } 230 296 defer rows.Close() 231 297 232 298 for rows.Next() { 233 299 var issue Issue 234 300 var createdAt string 235 - var metadata IssueMetadata 236 - 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 + ) 237 317 if err != nil { 238 - 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 + } 239 329 } 240 330 241 - createdTime, err := time.Parse(time.RFC3339, createdAt) 242 - if err != nil { 243 - return nil, err 331 + if deletedAt.Valid { 332 + if t, err := time.Parse(time.RFC3339, deletedAt.V); err == nil { 333 + issue.Deleted = &t 334 + } 244 335 } 245 - issue.Created = createdTime 246 - 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 + } 247 362 248 - 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) 249 368 } 250 369 251 - if err := rows.Err(); err != nil { 252 - 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 + } 253 375 } 254 376 377 + var issues []Issue 378 + for _, i := range issueMap { 379 + issues = append(issues, *i) 380 + } 381 + 382 + sort.Slice(issues, func(i, j int) bool { 383 + return issues[i].Created.After(issues[j].Created) 384 + }) 385 + 255 386 return issues, nil 256 387 } 257 388 ··· 302 433 var issue Issue 303 434 var issueCreatedAt string 304 435 err := rows.Scan( 305 - &issue.ID, 306 - &issue.OwnerDid, 436 + &issue.Id, 437 + &issue.Did, 307 438 &issue.RepoAt, 308 439 &issue.IssueId, 309 440 &issueCreatedAt, ··· 332 463 } 333 464 334 465 func GetIssues(e Execer, filters ...filter) ([]Issue, error) { 335 - return GetIssuesWithLimit(e, 0, filters...) 466 + return GetIssuesPaginated(e, pagination.FirstPage(), filters...) 336 467 } 337 468 338 469 // timeframe here is directly passed into the sql query filter, and any ··· 375 506 var issueCreatedAt, repoCreatedAt string 376 507 var repo Repo 377 508 err := rows.Scan( 378 - &issue.ID, 379 - &issue.OwnerDid, 509 + &issue.Id, 510 + &issue.Did, 380 511 &issue.Rkey, 381 512 &issue.RepoAt, 382 513 &issue.IssueId, ··· 406 537 } 407 538 repo.Created = repoCreatedTime 408 539 409 - issue.Metadata = &IssueMetadata{ 410 - Repo: &repo, 411 - } 412 - 413 540 issues = append(issues, issue) 414 541 } 415 542 ··· 426 553 427 554 var issue Issue 428 555 var createdAt string 429 - 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) 430 557 if err != nil { 431 558 return nil, err 432 559 } ··· 440 567 return &issue, nil 441 568 } 442 569 443 - func GetIssueWithComments(e Execer, repoAt syntax.ATURI, issueId int) (*Issue, []Comment, error) { 444 - query := `select id, owner_did, rkey, issue_id, created, title, body, open from issues where repo_at = ? and issue_id = ?` 445 - row := e.QueryRow(query, repoAt, issueId) 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 + ) 601 + if err != nil { 602 + return 0, err 603 + } 446 604 447 - var issue Issue 448 - var createdAt string 449 - err := row.Scan(&issue.ID, &issue.OwnerDid, &issue.Rkey, &issue.IssueId, &createdAt, &issue.Title, &issue.Body, &issue.Open) 605 + id, err := result.LastInsertId() 450 606 if err != nil { 451 - return nil, nil, err 607 + return 0, err 452 608 } 453 609 454 - createdTime, err := time.Parse(time.RFC3339, createdAt) 455 - if err != nil { 456 - return nil, nil, err 610 + return id, nil 611 + } 612 + 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()...) 457 619 } 458 - issue.Created = createdTime 459 620 460 - comments, err := GetComments(e, repoAt, issueId) 461 - if err != nil { 462 - return nil, nil, err 621 + whereClause := "" 622 + if conditions != nil { 623 + whereClause = " where " + strings.Join(conditions, " and ") 463 624 } 464 625 465 - return &issue, comments, nil 466 - } 626 + query := fmt.Sprintf(`update issue_comments set body = "", deleted = strftime('%%Y-%%m-%%dT%%H:%%M:%%SZ', 'now') %s`, whereClause) 467 627 468 - func NewIssueComment(e Execer, comment *Comment) error { 469 - query := `insert into comments (owner_did, repo_at, rkey, issue_id, comment_id, body) values (?, ?, ?, ?, ?, ?)` 470 - _, err := e.Exec( 471 - query, 472 - comment.OwnerDid, 473 - comment.RepoAt, 474 - comment.Rkey, 475 - comment.Issue, 476 - comment.CommentId, 477 - comment.Body, 478 - ) 628 + _, err := e.Exec(query, args...) 479 629 return err 480 630 } 481 631 482 - func GetComments(e Execer, repoAt syntax.ATURI, issueId int) ([]Comment, error) { 483 - 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 + } 641 + 642 + whereClause := "" 643 + if conditions != nil { 644 + whereClause = " where " + strings.Join(conditions, " and ") 645 + } 484 646 485 - rows, err := e.Query(` 647 + query := fmt.Sprintf(` 486 648 select 487 - owner_did, 488 - issue_id, 489 - comment_id, 649 + id, 650 + did, 490 651 rkey, 652 + issue_at, 653 + reply_to, 491 654 body, 492 655 created, 493 656 edited, 494 657 deleted 495 658 from 496 - comments 497 - where 498 - repo_at = ? and issue_id = ? 499 - order by 500 - created asc`, 501 - repoAt, 502 - issueId, 503 - ) 504 - if err == sql.ErrNoRows { 505 - return []Comment{}, nil 506 - } 659 + issue_comments 660 + %s 661 + `, whereClause) 662 + 663 + rows, err := e.Query(query, args...) 507 664 if err != nil { 508 665 return nil, err 509 666 } 510 - defer rows.Close() 511 667 512 668 for rows.Next() { 513 - var comment Comment 514 - var createdAt string 515 - var deletedAt, editedAt, rkey sql.NullString 516 - 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 + ) 517 683 if err != nil { 518 684 return nil, err 519 685 } 520 686 521 - createdAtTime, err := time.Parse(time.RFC3339, createdAt) 522 - if err != nil { 523 - 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 690 + } 691 + 692 + if t, err := time.Parse(time.RFC3339, created); err == nil { 693 + comment.Created = t 524 694 } 525 - comment.Created = &createdAtTime 526 695 527 - if deletedAt.Valid { 528 - deletedTime, err := time.Parse(time.RFC3339, deletedAt.String) 529 - if err != nil { 530 - return nil, err 696 + if edited.Valid { 697 + if t, err := time.Parse(time.RFC3339, edited.V); err == nil { 698 + comment.Edited = &t 531 699 } 532 - comment.Deleted = &deletedTime 533 700 } 534 701 535 - if editedAt.Valid { 536 - editedTime, err := time.Parse(time.RFC3339, editedAt.String) 537 - if err != nil { 538 - return nil, err 702 + if deleted.Valid { 703 + if t, err := time.Parse(time.RFC3339, deleted.V); err == nil { 704 + comment.Deleted = &t 539 705 } 540 - comment.Edited = &editedTime 541 706 } 542 707 543 - if rkey.Valid { 544 - comment.Rkey = rkey.String 708 + if replyTo.Valid { 709 + comment.ReplyTo = &replyTo.V 545 710 } 546 711 547 712 comments = append(comments, comment) 548 713 } 549 714 550 - if err := rows.Err(); err != nil { 715 + if err = rows.Err(); err != nil { 551 716 return nil, err 552 717 } 553 718 554 719 return comments, nil 555 720 } 556 721 557 - func GetComment(e Execer, repoAt syntax.ATURI, issueId, commentId int) (*Comment, error) { 558 - query := ` 559 - select 560 - owner_did, body, rkey, created, deleted, edited 561 - from 562 - comments where repo_at = ? and issue_id = ? and comment_id = ? 563 - ` 564 - row := e.QueryRow(query, repoAt, issueId, commentId) 565 - 566 - var comment Comment 567 - var createdAt string 568 - var deletedAt, editedAt, rkey sql.NullString 569 - err := row.Scan(&comment.OwnerDid, &comment.Body, &rkey, &createdAt, &deletedAt, &editedAt) 570 - if err != nil { 571 - return nil, err 572 - } 573 - 574 - createdTime, err := time.Parse(time.RFC3339, createdAt) 575 - if err != nil { 576 - return nil, err 577 - } 578 - comment.Created = &createdTime 579 - 580 - if deletedAt.Valid { 581 - deletedTime, err := time.Parse(time.RFC3339, deletedAt.String) 582 - if err != nil { 583 - return nil, err 584 - } 585 - comment.Deleted = &deletedTime 586 - } 587 - 588 - if editedAt.Valid { 589 - editedTime, err := time.Parse(time.RFC3339, editedAt.String) 590 - if err != nil { 591 - return nil, err 592 - } 593 - comment.Edited = &editedTime 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()...) 594 728 } 595 729 596 - if rkey.Valid { 597 - comment.Rkey = rkey.String 730 + whereClause := "" 731 + if conditions != nil { 732 + whereClause = " where " + strings.Join(conditions, " and ") 598 733 } 599 734 600 - comment.RepoAt = repoAt 601 - comment.Issue = issueId 602 - comment.CommentId = commentId 603 - 604 - return &comment, nil 605 - } 606 - 607 - func EditComment(e Execer, repoAt syntax.ATURI, issueId, commentId int, newBody string) error { 608 - _, err := e.Exec( 609 - ` 610 - update comments 611 - set body = ?, 612 - edited = strftime('%Y-%m-%dT%H:%M:%SZ', 'now') 613 - where repo_at = ? and issue_id = ? and comment_id = ? 614 - `, newBody, repoAt, issueId, commentId) 735 + query := fmt.Sprintf(`delete from issues %s`, whereClause) 736 + _, err := e.Exec(query, args...) 615 737 return err 616 738 } 617 739 618 - func DeleteComment(e Execer, repoAt syntax.ATURI, issueId, commentId int) error { 619 - _, err := e.Exec( 620 - ` 621 - update comments 622 - set body = "", 623 - deleted = strftime('%Y-%m-%dT%H:%M:%SZ', 'now') 624 - where repo_at = ? and issue_id = ? and comment_id = ? 625 - `, repoAt, issueId, commentId) 626 - return err 627 - } 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()...) 746 + } 628 747 629 - func UpdateCommentByRkey(e Execer, ownerDid, rkey, newBody string) error { 630 - _, err := e.Exec( 631 - ` 632 - update comments 633 - set body = ?, 634 - edited = strftime('%Y-%m-%dT%H:%M:%SZ', 'now') 635 - where owner_did = ? and rkey = ? 636 - `, newBody, ownerDid, rkey) 637 - return err 638 - } 748 + whereClause := "" 749 + if conditions != nil { 750 + whereClause = " where " + strings.Join(conditions, " and ") 751 + } 639 752 640 - func DeleteCommentByRkey(e Execer, ownerDid, rkey string) error { 641 - _, err := e.Exec( 642 - ` 643 - update comments 644 - set body = "", 645 - deleted = strftime('%Y-%m-%dT%H:%M:%SZ', 'now') 646 - where owner_did = ? and rkey = ? 647 - `, ownerDid, rkey) 648 - return err 649 - } 650 - 651 - func UpdateIssueByRkey(e Execer, ownerDid, rkey, title, body string) error { 652 - _, err := e.Exec(`update issues set title = ?, body = ? where owner_did = ? and rkey = ?`, title, body, ownerDid, rkey) 753 + query := fmt.Sprintf(`update issues set open = 0 %s`, whereClause) 754 + _, err := e.Exec(query, args...) 653 755 return err 654 756 } 655 757 656 - func DeleteIssueByRkey(e Execer, ownerDid, rkey string) error { 657 - _, err := e.Exec(`delete from issues where owner_did = ? and rkey = ?`, ownerDid, rkey) 658 - return err 659 - } 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 + } 660 765 661 - func CloseIssue(e Execer, repoAt syntax.ATURI, issueId int) error { 662 - _, err := e.Exec(`update issues set open = 0 where repo_at = ? and issue_id = ?`, repoAt, issueId) 663 - return err 664 - } 766 + whereClause := "" 767 + if conditions != nil { 768 + whereClause = " where " + strings.Join(conditions, " and ") 769 + } 665 770 666 - func ReopenIssue(e Execer, repoAt syntax.ATURI, issueId int) error { 667 - _, 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...) 668 773 return err 669 774 } 670 775
+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 }