forked from tangled.org/core
Monorepo for Tangled

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 32dbd785 1fe818b5

verified
Changed files
+270 -238
appview
+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
··· 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 }