This repository has no description
0

Configure Feed

Select the types of activity you want to include in your feed.

core / appview / db / repos.go
22 kB 864 lines
1package db 2 3import ( 4 "context" 5 "database/sql" 6 "errors" 7 "fmt" 8 "log" 9 "slices" 10 "strings" 11 "time" 12 13 "github.com/bluesky-social/indigo/atproto/syntax" 14 "tangled.org/core/api/tangled" 15 "tangled.org/core/appview/models" 16 "tangled.org/core/appview/pagination" 17 "tangled.org/core/orm" 18) 19 20func RenameRepo(tx *sql.Tx, did, oldRkey, newRkey, newName string) error { 21 newAtURI := fmt.Sprintf("at://%s/sh.tangled.repo/%s", did, newRkey) 22 23 res, err := tx.Exec( 24 `update repos set rkey = ?, name = ?, at_uri = ? where did = ? and rkey = ?`, 25 newRkey, newName, newAtURI, did, oldRkey, 26 ) 27 if err != nil { 28 return fmt.Errorf("update repos row: %w", err) 29 } 30 if n, _ := res.RowsAffected(); n == 0 { 31 return fmt.Errorf("no repo row found for did=%s rkey=%s", did, oldRkey) 32 } 33 34 if _, err := tx.Exec( 35 `update pipelines set repo_name = ? where repo_owner = ? and repo_name = ?`, 36 newRkey, did, oldRkey, 37 ); err != nil { 38 return fmt.Errorf("rename pipelines.repo_name: %w", err) 39 } 40 41 return nil 42} 43 44func UpdateRepoDisplayName(e Execer, did, rkey, newName string) error { 45 _, err := e.Exec( 46 `update repos set name = ? where did = ? and rkey = ?`, 47 newName, did, rkey, 48 ) 49 return err 50} 51 52func RecordRepoRename(e Execer, ownerDid, oldRkey, repoDid string) error { 53 _, err := e.Exec( 54 `insert into repo_renames (owner_did, old_rkey, repo_did) 55 values (?, ?, ?) 56 on conflict(owner_did, old_rkey) do update set 57 repo_did = excluded.repo_did, 58 renamed_at = strftime('%Y-%m-%dT%H:%M:%SZ', 'now')`, 59 ownerDid, oldRkey, repoDid, 60 ) 61 return err 62} 63 64func DeleteRepoRename(e Execer, ownerDid, oldRkey string) error { 65 _, err := e.Exec( 66 `delete from repo_renames where owner_did = ? and old_rkey = ?`, 67 ownerDid, oldRkey, 68 ) 69 return err 70} 71 72func LookupRepoRename(e Execer, ownerDid, oldRkey string) (*models.Repo, error) { 73 var repoDid string 74 err := e.QueryRow( 75 `select repo_did from repo_renames where owner_did = ? and old_rkey = ?`, 76 ownerDid, oldRkey, 77 ).Scan(&repoDid) 78 if err != nil { 79 return nil, err 80 } 81 82 repo, err := GetRepoByDid(e, repoDid) 83 if err != nil { 84 return nil, err 85 } 86 return repo, nil 87} 88 89func GetRepos(e Execer, filters ...orm.Filter) ([]models.Repo, error) { 90 return GetReposPaginated(e, pagination.Page{}, filters...) 91} 92 93func GetReposPaginated(e Execer, page pagination.Page, filters ...orm.Filter) ([]models.Repo, error) { 94 var conditions []string 95 var args []any 96 for _, filter := range filters { 97 conditions = append(conditions, filter.Condition()) 98 args = append(args, filter.Arg()...) 99 } 100 101 whereClause := "" 102 if conditions != nil { 103 whereClause = " where " + strings.Join(conditions, " and ") 104 } 105 106 pageClause := "" 107 if page.Limit != 0 { 108 pageClause = fmt.Sprintf(" limit %d offset %d", page.Limit, page.Offset) 109 } 110 111 // main query to get repos with pagination 112 query := fmt.Sprintf(` 113 select 114 id, 115 did, 116 name, 117 knot, 118 rkey, 119 created, 120 description, 121 website, 122 topics, 123 source, 124 spindle, 125 repo_did 126 from repos 127 %s 128 order by created desc 129 %s 130 `, whereClause, pageClause) 131 132 rows, err := e.Query(query, args...) 133 if err != nil { 134 return nil, err 135 } 136 defer rows.Close() 137 138 repoMap := make(map[string]*models.Repo) 139 for rows.Next() { 140 var repo models.Repo 141 var createdAt string 142 var description, website, topicStr, source, spindle, repoDid sql.NullString 143 144 err := rows.Scan( 145 &repo.Id, 146 &repo.Did, 147 &repo.Name, 148 &repo.Knot, 149 &repo.Rkey, 150 &createdAt, 151 &description, 152 &website, 153 &topicStr, 154 &source, 155 &spindle, 156 &repoDid, 157 ) 158 if err != nil { 159 return nil, err 160 } 161 162 // parse created timestamp 163 if t, err := time.Parse(time.RFC3339, createdAt); err == nil { 164 repo.Created = t 165 } 166 167 // handle nullable fields 168 if description.Valid { 169 repo.Description = description.String 170 } 171 if website.Valid { 172 repo.Website = website.String 173 } 174 if topicStr.Valid { 175 repo.Topics = strings.Fields(topicStr.String) 176 } 177 if source.Valid { 178 repo.Source = source.String 179 } 180 if spindle.Valid { 181 repo.Spindle = spindle.String 182 } 183 if repoDid.Valid { 184 repo.RepoDid = repoDid.String 185 } 186 187 repo.RepoStats = &models.RepoStats{} 188 repoMap[repo.RepoDid] = &repo 189 } 190 191 if err = rows.Err(); err != nil { 192 return nil, err 193 } 194 195 // if no repos, return early 196 if len(repoMap) == 0 { 197 return nil, nil 198 } 199 200 // build IN clause for related queries 201 inClause := strings.TrimSuffix(strings.Repeat("?, ", len(repoMap)), ", ") 202 args = make([]any, len(repoMap)) 203 i := 0 204 for _, r := range repoMap { 205 args[i] = r.RepoDid 206 i++ 207 } 208 209 // get labels for all repos 210 { 211 labelsQuery := fmt.Sprintf( 212 `select repo_did, label_at from repo_labels where repo_did in (%s)`, 213 inClause, 214 ) 215 216 rows, err = e.Query(labelsQuery, args...) 217 if err != nil { 218 return nil, fmt.Errorf("failed to execute repo_labels query: %w", err) 219 } 220 defer rows.Close() 221 222 for rows.Next() { 223 var repoDid, labelat string 224 if err := rows.Scan(&repoDid, &labelat); err != nil { 225 log.Println("err", err) 226 continue 227 } 228 if r, ok := repoMap[repoDid]; ok { 229 r.Labels = append(r.Labels, labelat) 230 } 231 } 232 if err = rows.Err(); err != nil { 233 return nil, fmt.Errorf("failed to execute repo_labels query: %w", err) 234 } 235 } 236 237 // get primary language for all repos 238 { 239 languageQuery := fmt.Sprintf(` 240 select repo_did, language 241 from ( 242 select 243 repo_did, language, 244 row_number() over ( 245 partition by repo_did 246 order by bytes desc 247 ) as rn 248 from repo_languages 249 where repo_did in (%s) 250 and is_default_ref = 1 251 and language <> '' 252 ) 253 where rn = 1 254 `, inClause) 255 256 rows, err = e.Query(languageQuery, args...) 257 if err != nil { 258 return nil, fmt.Errorf("failed to execute lang query: %w", err) 259 } 260 defer rows.Close() 261 262 for rows.Next() { 263 var repoDid, lang string 264 if err := rows.Scan(&repoDid, &lang); err != nil { 265 log.Println("err", err) 266 continue 267 } 268 if r, ok := repoMap[repoDid]; ok { 269 r.RepoStats.Language = lang 270 } 271 } 272 if err = rows.Err(); err != nil { 273 return nil, fmt.Errorf("failed to execute lang query: %w", err) 274 } 275 } 276 277 // get star counts 278 { 279 starCountQuery := fmt.Sprintf( 280 `select subject, count(1) from stars where subject_type = 'repo' and subject in (%s) group by subject`, 281 inClause, 282 ) 283 284 rows, err = e.Query(starCountQuery, args...) 285 if err != nil { 286 return nil, fmt.Errorf("failed to execute star-count query: %w", err) 287 } 288 defer rows.Close() 289 290 for rows.Next() { 291 var repoDid string 292 var count int 293 if err := rows.Scan(&repoDid, &count); err != nil { 294 log.Println("err", "err", err) 295 continue 296 } 297 if r, ok := repoMap[repoDid]; ok { 298 r.RepoStats.StarCount = count 299 } 300 } 301 if err = rows.Err(); err != nil { 302 return nil, fmt.Errorf("failed to execute star-count query: %w", err) 303 } 304 } 305 306 // get issue counts 307 { 308 issueCountQuery := fmt.Sprintf(` 309 select 310 repo_did, 311 count(case when open = 1 then 1 end) as open_count, 312 count(case when open = 0 then 1 end) as closed_count 313 from issues 314 where repo_did in (%s) 315 group by repo_did 316 `, inClause) 317 318 rows, err = e.Query(issueCountQuery, args...) 319 if err != nil { 320 return nil, fmt.Errorf("failed to execute issue-count query: %w", err) 321 } 322 defer rows.Close() 323 324 for rows.Next() { 325 var repoDid string 326 var open, closed int 327 if err := rows.Scan(&repoDid, &open, &closed); err != nil { 328 log.Println("err", err) 329 continue 330 } 331 if r, ok := repoMap[repoDid]; ok { 332 r.RepoStats.IssueCount.Open = open 333 r.RepoStats.IssueCount.Closed = closed 334 } 335 } 336 if err = rows.Err(); err != nil { 337 return nil, fmt.Errorf("failed to execute issue-count query: %w", err) 338 } 339 } 340 341 // get pull counts 342 { 343 pullCountQuery := fmt.Sprintf(` 344 select 345 repo_did, 346 count(case when state = ? then 1 end) as open_count, 347 count(case when state = ? then 1 end) as merged_count, 348 count(case when state = ? then 1 end) as closed_count, 349 count(case when state = ? then 1 end) as deleted_count 350 from pulls 351 where repo_did in (%s) 352 group by repo_did 353 `, inClause) 354 355 pullArgs := append([]any{ 356 models.PullOpen, 357 models.PullMerged, 358 models.PullClosed, 359 models.PullAbandoned, 360 }, args...) 361 362 rows, err = e.Query(pullCountQuery, pullArgs...) 363 if err != nil { 364 return nil, fmt.Errorf("failed to execute pulls-count query: %w", err) 365 } 366 defer rows.Close() 367 368 for rows.Next() { 369 var repoDid string 370 var open, merged, closed, deleted int 371 if err := rows.Scan(&repoDid, &open, &merged, &closed, &deleted); err != nil { 372 log.Println("err", "err", err) 373 continue 374 } 375 if r, ok := repoMap[repoDid]; ok { 376 r.RepoStats.PullCount.Open = open 377 r.RepoStats.PullCount.Merged = merged 378 r.RepoStats.PullCount.Closed = closed 379 r.RepoStats.PullCount.Deleted = deleted 380 } 381 } 382 if err = rows.Err(); err != nil { 383 return nil, fmt.Errorf("failed to execute pulls-count query: %w", err) 384 } 385 } 386 387 // get forks — only query repos with a non-empty repo_did, since source 388 // stores the upstream's repo_did and an empty string would match all 389 var forksArgs []any 390 for _, r := range repoMap { 391 if r.RepoDid != "" { 392 forksArgs = append(forksArgs, r.RepoDid) 393 } 394 } 395 396 if len(forksArgs) > 0 { 397 forksInClause := strings.TrimSuffix(strings.Repeat("?, ", len(forksArgs)), ", ") 398 399 forksCountQuery := fmt.Sprintf( 400 `select source, count(1) from repos where source in (%s) group by source`, 401 forksInClause, 402 ) 403 404 rows, err = e.Query(forksCountQuery, forksArgs...) 405 if err != nil { 406 return nil, fmt.Errorf("failed to execute fork-count query: %w", err) 407 } 408 defer rows.Close() 409 410 for rows.Next() { 411 var repodid string 412 var count int 413 if err := rows.Scan(&repodid, &count); err != nil { 414 log.Println("failed to scan fork count", "err", err) 415 continue 416 } 417 418 if r, ok := repoMap[repodid]; ok { 419 r.RepoStats.ForkCount = count 420 } 421 } 422 if err = rows.Err(); err != nil { 423 return nil, fmt.Errorf("failed to execute fork-count query: %w", err) 424 } 425 } 426 427 var repos []models.Repo 428 for _, r := range repoMap { 429 repos = append(repos, *r) 430 } 431 432 // sort by created timestamp (desc) 433 slices.SortFunc(repos, func(a, b models.Repo) int { 434 if a.Created.After(b.Created) { 435 return -1 436 } 437 return 1 438 }) 439 440 return repos, nil 441} 442 443// helper to get exactly one repo 444func GetRepo(e Execer, filters ...orm.Filter) (*models.Repo, error) { 445 repos, err := GetReposPaginated(e, pagination.Page{Limit: 1}, filters...) 446 if err != nil { 447 return nil, err 448 } 449 450 if repos == nil { 451 return nil, sql.ErrNoRows 452 } 453 454 if len(repos) != 1 { 455 return nil, fmt.Errorf("too few rows returned") 456 } 457 458 return &repos[0], nil 459} 460 461func CountRepos(e Execer, filters ...orm.Filter) (int64, error) { 462 var conditions []string 463 var args []any 464 for _, filter := range filters { 465 conditions = append(conditions, filter.Condition()) 466 args = append(args, filter.Arg()...) 467 } 468 469 whereClause := "" 470 if conditions != nil { 471 whereClause = " where " + strings.Join(conditions, " and ") 472 } 473 474 repoQuery := fmt.Sprintf(`select count(1) from repos %s`, whereClause) 475 var count int64 476 err := e.QueryRow(repoQuery, args...).Scan(&count) 477 478 if !errors.Is(err, sql.ErrNoRows) && err != nil { 479 return 0, err 480 } 481 482 return count, nil 483} 484 485func GetRepoByAtUri(e Execer, atUri string) (*models.Repo, error) { 486 var repo models.Repo 487 var nullableDescription sql.NullString 488 var nullableWebsite sql.NullString 489 var nullableTopicStr sql.NullString 490 var nullableRepoDid sql.NullString 491 var nullableSource sql.NullString 492 var nullableSpindle sql.NullString 493 494 row := e.QueryRow(`select id, did, name, knot, created, rkey, description, website, topics, source, spindle, repo_did from repos where at_uri = ?`, atUri) 495 496 var createdAt string 497 if err := row.Scan(&repo.Id, &repo.Did, &repo.Name, &repo.Knot, &createdAt, &repo.Rkey, &nullableDescription, &nullableWebsite, &nullableTopicStr, &nullableSource, &nullableSpindle, &nullableRepoDid); err != nil { 498 return nil, err 499 } 500 createdAtTime, _ := time.Parse(time.RFC3339, createdAt) 501 repo.Created = createdAtTime 502 503 if nullableDescription.Valid { 504 repo.Description = nullableDescription.String 505 } 506 if nullableWebsite.Valid { 507 repo.Website = nullableWebsite.String 508 } 509 if nullableTopicStr.Valid { 510 repo.Topics = strings.Fields(nullableTopicStr.String) 511 } 512 if nullableSource.Valid { 513 repo.Source = nullableSource.String 514 } 515 if nullableSpindle.Valid { 516 repo.Spindle = nullableSpindle.String 517 } 518 if nullableRepoDid.Valid { 519 repo.RepoDid = nullableRepoDid.String 520 } 521 522 return &repo, nil 523} 524 525func PutRepo(tx *sql.Tx, repo models.Repo) error { 526 var repoDid *string 527 if repo.RepoDid != "" { 528 repoDid = &repo.RepoDid 529 } 530 _, err := tx.Exec( 531 `update repos 532 set name = ?, knot = ?, description = ?, website = ?, topics = ?, repo_did = coalesce(?, repo_did) 533 where did = ? and rkey = ? 534 `, 535 repo.Name, repo.Knot, repo.Description, repo.Website, repo.TopicStr(), repoDid, repo.Did, repo.Rkey, 536 ) 537 return err 538} 539 540func AddRepo(tx *sql.Tx, repo *models.Repo) error { 541 var repoDid *string 542 if repo.RepoDid != "" { 543 repoDid = &repo.RepoDid 544 } 545 result, err := tx.Exec( 546 `insert into repos 547 (did, name, knot, rkey, at_uri, description, website, topics, source, repo_did) 548 values (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, 549 repo.Did, repo.Name, repo.Knot, repo.Rkey, repo.RepoAt().String(), repo.Description, repo.Website, repo.TopicStr(), repo.Source, repoDid, 550 ) 551 if err != nil { 552 return fmt.Errorf("failed to insert repo: %w", err) 553 } 554 555 id, err := result.LastInsertId() 556 if err != nil { 557 return fmt.Errorf("failed to get last insert id: %w", err) 558 } 559 repo.Id = id 560 561 for _, dl := range repo.Labels { 562 if err := SubscribeLabel(tx, &models.RepoLabel{ 563 RepoDid: syntax.DID(repo.RepoDid), 564 LabelAt: syntax.ATURI(dl), 565 }); err != nil { 566 return fmt.Errorf("failed to subscribe to label: %w", err) 567 } 568 } 569 570 return nil 571} 572 573func RemoveRepo(e Execer, did, rkey string) error { 574 _, err := e.Exec(`delete from repos where did = ? and rkey = ?`, did, rkey) 575 return err 576} 577 578func RemoveReposByKnot(e Execer, knot string) error { 579 _, err := e.Exec(`delete from repos where knot = ?`, knot) 580 return err 581} 582 583func GetRepoSource(e Execer, repoDid string) (string, error) { 584 var nullableSource sql.NullString 585 err := e.QueryRow(`select source from repos where repo_did = ?`, repoDid).Scan(&nullableSource) 586 if err != nil { 587 return "", err 588 } 589 return nullableSource.String, nil 590} 591 592func GetRepoSourceRepo(e Execer, repoDid string) (*models.Repo, error) { 593 source, err := GetRepoSource(e, repoDid) 594 if source == "" || errors.Is(err, sql.ErrNoRows) { 595 return nil, nil 596 } 597 if err != nil { 598 return nil, err 599 } 600 if strings.HasPrefix(source, "did:") { 601 return GetRepoByDid(e, source) 602 } 603 return GetRepoByAtUri(e, source) 604} 605 606func GetForksByDid(e Execer, did string) ([]models.Repo, error) { 607 var repos []models.Repo 608 609 rows, err := e.Query( 610 `select distinct r.id, r.did, r.name, r.knot, r.rkey, r.description, r.website, r.created, r.source, r.repo_did 611 from repos r 612 left join collaborators c on r.repo_did = c.repo_did 613 where (r.did = ? or c.subject_did = ?) 614 and r.source is not null 615 and r.source != '' 616 order by r.created desc`, 617 did, did, 618 ) 619 if err != nil { 620 return nil, err 621 } 622 defer rows.Close() 623 624 for rows.Next() { 625 var repo models.Repo 626 var createdAt string 627 var nullableDescription sql.NullString 628 var nullableWebsite sql.NullString 629 var nullableSource sql.NullString 630 var nullableRepoDid sql.NullString 631 632 err := rows.Scan(&repo.Id, &repo.Did, &repo.Name, &repo.Knot, &repo.Rkey, &nullableDescription, &nullableWebsite, &createdAt, &nullableSource, &nullableRepoDid) 633 if err != nil { 634 return nil, err 635 } 636 637 if nullableDescription.Valid { 638 repo.Description = nullableDescription.String 639 } 640 if nullableWebsite.Valid { 641 repo.Website = nullableWebsite.String 642 } 643 644 if nullableSource.Valid { 645 repo.Source = nullableSource.String 646 } 647 if nullableRepoDid.Valid { 648 repo.RepoDid = nullableRepoDid.String 649 } 650 651 createdAtTime, err := time.Parse(time.RFC3339, createdAt) 652 if err != nil { 653 repo.Created = time.Now() 654 } else { 655 repo.Created = createdAtTime 656 } 657 658 repos = append(repos, repo) 659 } 660 661 if err := rows.Err(); err != nil { 662 return nil, err 663 } 664 665 return repos, nil 666} 667 668func GetRepoByDid(e Execer, repoDid string) (*models.Repo, error) { 669 return GetRepo(e, orm.FilterEq("repo_did", repoDid)) 670} 671 672func GetForkByRepoDid(e Execer, repoDid string) (*models.Repo, error) { 673 return GetRepo(e, orm.FilterEq("repo_did", repoDid), orm.FilterNotEq("source", "")) 674} 675 676// TODO: just queue every legacy records regardless of target repo has a DID or not. 677// doable after we have `repo_did` column in db for each tables. 678func EnqueuePdsRewritesForRepo(tx *sql.Tx, repoDid, repoAtUri string) error { 679 type record struct { 680 userDidCol string 681 table string 682 nsid syntax.NSID 683 fkCol string 684 fkVal string 685 } 686 sources := []record{ 687 {"did", "repos", tangled.RepoNSID, "at_uri", repoAtUri}, 688 {"did", "issues", tangled.RepoIssueNSID, "repo_did", repoDid}, 689 {"owner_did", "pulls", tangled.RepoPullNSID, "repo_did", repoDid}, 690 {"did", "collaborators", tangled.RepoCollaboratorNSID, "repo_did", repoDid}, 691 {"did", "artifacts", tangled.RepoArchiveNSID, "repo_did", repoDid}, 692 {"did", "stars", tangled.FeedStarNSID, "subject", repoDid}, 693 } 694 695 for _, src := range sources { 696 rows, err := tx.Query( 697 fmt.Sprintf(`SELECT %s, rkey FROM %s WHERE %s = ?`, src.userDidCol, src.table, src.fkCol), 698 src.fkVal, 699 ) 700 if err != nil { 701 return fmt.Errorf("query %s for pds rewrites: %w", src.table, err) 702 } 703 704 var pairs []struct{ did, rkey string } 705 for rows.Next() { 706 var d string 707 var r sql.NullString 708 if scanErr := rows.Scan(&d, &r); scanErr != nil { 709 rows.Close() 710 return fmt.Errorf("scan %s for pds rewrites: %w", src.table, scanErr) 711 } 712 if !r.Valid { 713 continue 714 } 715 pairs = append(pairs, struct{ did, rkey string }{d, r.String}) 716 } 717 rows.Close() 718 if rowsErr := rows.Err(); rowsErr != nil { 719 return fmt.Errorf("iterate %s for pds rewrites: %w", src.table, rowsErr) 720 } 721 722 for _, p := range pairs { 723 if err := EnqueuePdsRecordMigration(context.Background(), tx, "add-repo-did", syntax.DID(p.did), src.nsid, syntax.RecordKey(p.rkey)); err != nil { 724 return fmt.Errorf("enqueue pds rewrite for %s/%s: %w", src.table, p.rkey, err) 725 } 726 } 727 } 728 729 profileRows, err := tx.Query( 730 `SELECT DISTINCT did FROM profile_pinned_repositories WHERE pin = ?`, 731 repoAtUri, 732 ) 733 if err != nil { 734 return fmt.Errorf("query profile_pinned_repositories for pds rewrites: %w", err) 735 } 736 var profileDids []string 737 for profileRows.Next() { 738 var d string 739 if scanErr := profileRows.Scan(&d); scanErr != nil { 740 profileRows.Close() 741 return fmt.Errorf("scan profile_pinned_repositories for pds rewrites: %w", scanErr) 742 } 743 profileDids = append(profileDids, d) 744 } 745 profileRows.Close() 746 if profileRowsErr := profileRows.Err(); profileRowsErr != nil { 747 return fmt.Errorf("iterate profile_pinned_repositories for pds rewrites: %w", profileRowsErr) 748 } 749 750 for _, d := range profileDids { 751 if err := EnqueuePdsRecordMigration(context.Background(), tx, "add-repo-did", syntax.DID(d), tangled.ActorProfileNSID, "self"); err != nil { 752 return fmt.Errorf("enqueue pds rewrite for profile/%s: %w", d, err) 753 } 754 } 755 756 return nil 757} 758 759func CascadeRepoDid(tx *sql.Tx, repoAtUri, repoDid string) error { 760 _, err := tx.Exec( 761 `UPDATE repos SET repo_did = ? WHERE at_uri = ?`, 762 repoDid, repoAtUri, 763 ) 764 if err != nil { 765 return fmt.Errorf("cascade repo_did to repos: %w", err) 766 } 767 768 _, err = tx.Exec( 769 `UPDATE repos SET source = ? WHERE source = ?`, 770 repoDid, repoAtUri, 771 ) 772 if err != nil { 773 return fmt.Errorf("cascade repo_did to repos.source: %w", err) 774 } 775 776 return nil 777} 778 779func UpdateDescription(e Execer, repoDid, newDescription string) error { 780 _, err := e.Exec( 781 `update repos set description = ? where repo_did = ?`, newDescription, repoDid) 782 return err 783} 784 785func UpdateSpindle(e Execer, repoDid string, spindle *string) error { 786 _, err := e.Exec( 787 `update repos set spindle = ? where repo_did = ?`, spindle, repoDid) 788 return err 789} 790 791func SubscribeLabel(e Execer, rl *models.RepoLabel) error { 792 query := `insert or ignore into repo_labels (repo_did, label_at) values (?, ?)` 793 794 _, err := e.Exec(query, string(rl.RepoDid), rl.LabelAt.String()) 795 return err 796} 797 798func UnsubscribeLabel(e Execer, filters ...orm.Filter) error { 799 var conditions []string 800 var args []any 801 for _, filter := range filters { 802 conditions = append(conditions, filter.Condition()) 803 args = append(args, filter.Arg()...) 804 } 805 806 whereClause := "" 807 if conditions != nil { 808 whereClause = " where " + strings.Join(conditions, " and ") 809 } 810 811 query := fmt.Sprintf(`delete from repo_labels %s`, whereClause) 812 _, err := e.Exec(query, args...) 813 return err 814} 815 816func GetRepoLabels(e Execer, filters ...orm.Filter) ([]models.RepoLabel, error) { 817 var conditions []string 818 var args []any 819 for _, filter := range filters { 820 conditions = append(conditions, filter.Condition()) 821 args = append(args, filter.Arg()...) 822 } 823 824 whereClause := "" 825 if conditions != nil { 826 whereClause = " where " + strings.Join(conditions, " and ") 827 } 828 829 query := fmt.Sprintf(`select id, repo_did, label_at from repo_labels %s`, whereClause) 830 831 rows, err := e.Query(query, args...) 832 if err != nil { 833 return nil, err 834 } 835 defer rows.Close() 836 837 var labels []models.RepoLabel 838 for rows.Next() { 839 var label models.RepoLabel 840 841 err := rows.Scan(&label.Id, &label.RepoDid, &label.LabelAt) 842 if err != nil { 843 return nil, err 844 } 845 846 labels = append(labels, label) 847 } 848 849 if err = rows.Err(); err != nil { 850 return nil, err 851 } 852 853 return labels, nil 854} 855 856func GetForkCount(e Execer, sourceDID string) (int, error) { 857 forks := 0 858 err := e.QueryRow( 859 `select count(source) from repos where source = ?`, sourceDID).Scan(&forks) 860 if err != nil { 861 return 0, err 862 } 863 return forks, nil 864}