Monorepo for Tangled tangled.org
2

Configure Feed

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

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 labelsQuery := fmt.Sprintf( 211 `select repo_did, label_at from repo_labels where repo_did in (%s)`, 212 inClause, 213 ) 214 215 rows, err = e.Query(labelsQuery, args...) 216 if err != nil { 217 return nil, err 218 } 219 defer rows.Close() 220 221 for rows.Next() { 222 var repoDid, labelat string 223 if err := rows.Scan(&repoDid, &labelat); err != nil { 224 continue 225 } 226 if r, ok := repoMap[repoDid]; ok { 227 r.Labels = append(r.Labels, labelat) 228 } 229 } 230 231 // get primary language for all repos 232 languageQuery := fmt.Sprintf(` 233 select repo_did, language 234 from ( 235 select 236 repo_did, language, 237 row_number() over ( 238 partition by repo_did 239 order by bytes desc 240 ) as rn 241 from repo_languages 242 where repo_did in (%s) 243 and is_default_ref = 1 244 and language <> '' 245 ) 246 where rn = 1 247 `, inClause) 248 249 rows, err = e.Query(languageQuery, args...) 250 if err != nil { 251 return nil, fmt.Errorf("failed to execute lang query: %w", err) 252 } 253 defer rows.Close() 254 255 for rows.Next() { 256 var repoDid, lang string 257 if err := rows.Scan(&repoDid, &lang); err != nil { 258 log.Println("err", "err", err) 259 continue 260 } 261 if r, ok := repoMap[repoDid]; ok { 262 r.RepoStats.Language = lang 263 } 264 } 265 if err = rows.Err(); err != nil { 266 return nil, fmt.Errorf("failed to execute lang query: %w", err) 267 } 268 269 // get star counts 270 starCountQuery := fmt.Sprintf( 271 `select subject, count(1) from stars where subject_type = 'repo' and subject in (%s) group by subject`, 272 inClause, 273 ) 274 275 rows, err = e.Query(starCountQuery, args...) 276 if err != nil { 277 return nil, fmt.Errorf("failed to execute star-count query: %w", err) 278 } 279 defer rows.Close() 280 281 for rows.Next() { 282 var repoDid string 283 var count int 284 if err := rows.Scan(&repoDid, &count); err != nil { 285 log.Println("err", "err", err) 286 continue 287 } 288 if r, ok := repoMap[repoDid]; ok { 289 r.RepoStats.StarCount = count 290 } 291 } 292 if err = rows.Err(); err != nil { 293 return nil, fmt.Errorf("failed to execute star-count query: %w", err) 294 } 295 296 // get issue counts 297 issueCountQuery := fmt.Sprintf(` 298 select 299 repo_did, 300 count(case when open = 1 then 1 end) as open_count, 301 count(case when open = 0 then 1 end) as closed_count 302 from issues 303 where repo_did in (%s) 304 group by repo_did 305 `, inClause) 306 307 rows, err = e.Query(issueCountQuery, args...) 308 if err != nil { 309 return nil, fmt.Errorf("failed to execute issue-count query: %w", err) 310 } 311 defer rows.Close() 312 313 for rows.Next() { 314 var repoDid string 315 var open, closed int 316 if err := rows.Scan(&repoDid, &open, &closed); err != nil { 317 log.Println("err", "err", err) 318 continue 319 } 320 if r, ok := repoMap[repoDid]; ok { 321 r.RepoStats.IssueCount.Open = open 322 r.RepoStats.IssueCount.Closed = closed 323 } 324 } 325 if err = rows.Err(); err != nil { 326 return nil, fmt.Errorf("failed to execute issue-count query: %w", err) 327 } 328 329 // get pull counts 330 pullCountQuery := fmt.Sprintf(` 331 select 332 repo_did, 333 count(case when state = ? then 1 end) as open_count, 334 count(case when state = ? then 1 end) as merged_count, 335 count(case when state = ? then 1 end) as closed_count, 336 count(case when state = ? then 1 end) as deleted_count 337 from pulls 338 where repo_did in (%s) 339 group by repo_did 340 `, inClause) 341 342 pullArgs := append([]any{ 343 models.PullOpen, 344 models.PullMerged, 345 models.PullClosed, 346 models.PullAbandoned, 347 }, args...) 348 349 rows, err = e.Query(pullCountQuery, pullArgs...) 350 if err != nil { 351 return nil, fmt.Errorf("failed to execute pulls-count query: %w", err) 352 } 353 defer rows.Close() 354 355 for rows.Next() { 356 var repoDid string 357 var open, merged, closed, deleted int 358 if err := rows.Scan(&repoDid, &open, &merged, &closed, &deleted); err != nil { 359 log.Println("err", "err", err) 360 continue 361 } 362 if r, ok := repoMap[repoDid]; ok { 363 r.RepoStats.PullCount.Open = open 364 r.RepoStats.PullCount.Merged = merged 365 r.RepoStats.PullCount.Closed = closed 366 r.RepoStats.PullCount.Deleted = deleted 367 } 368 } 369 if err = rows.Err(); err != nil { 370 return nil, fmt.Errorf("failed to execute pulls-count query: %w", err) 371 } 372 373 // get forks — only query repos with a non-empty repo_did, since source 374 // stores the upstream's repo_did and an empty string would match all 375 var forksArgs []any 376 for _, r := range repoMap { 377 if r.RepoDid != "" { 378 forksArgs = append(forksArgs, r.RepoDid) 379 } 380 } 381 382 if len(forksArgs) > 0 { 383 forksInClause := strings.TrimSuffix(strings.Repeat("?, ", len(forksArgs)), ", ") 384 385 forksCountQuery := fmt.Sprintf( 386 `select source, count(1) from repos where source in (%s) group by source`, 387 forksInClause, 388 ) 389 390 rows, err = e.Query(forksCountQuery, forksArgs...) 391 if err != nil { 392 return nil, fmt.Errorf("failed to execute fork-count query: %w", err) 393 } 394 defer rows.Close() 395 396 for rows.Next() { 397 var repodid string 398 var count int 399 if err := rows.Scan(&repodid, &count); err != nil { 400 log.Println("failed to scan fork count", "err", err) 401 continue 402 } 403 404 if r, ok := repoMap[repodid]; ok { 405 r.RepoStats.ForkCount = count 406 } 407 } 408 if err = rows.Err(); err != nil { 409 return nil, fmt.Errorf("failed to execute fork-count query: %w", err) 410 } 411 } 412 413 var repos []models.Repo 414 for _, r := range repoMap { 415 repos = append(repos, *r) 416 } 417 418 // sort by created timestamp (desc) 419 slices.SortFunc(repos, func(a, b models.Repo) int { 420 if a.Created.After(b.Created) { 421 return -1 422 } 423 return 1 424 }) 425 426 return repos, nil 427} 428 429// helper to get exactly one repo 430func GetRepo(e Execer, filters ...orm.Filter) (*models.Repo, error) { 431 repos, err := GetReposPaginated(e, pagination.Page{Limit: 1}, filters...) 432 if err != nil { 433 return nil, err 434 } 435 436 if repos == nil { 437 return nil, sql.ErrNoRows 438 } 439 440 if len(repos) != 1 { 441 return nil, fmt.Errorf("too few rows returned") 442 } 443 444 return &repos[0], nil 445} 446 447func CountRepos(e Execer, filters ...orm.Filter) (int64, error) { 448 var conditions []string 449 var args []any 450 for _, filter := range filters { 451 conditions = append(conditions, filter.Condition()) 452 args = append(args, filter.Arg()...) 453 } 454 455 whereClause := "" 456 if conditions != nil { 457 whereClause = " where " + strings.Join(conditions, " and ") 458 } 459 460 repoQuery := fmt.Sprintf(`select count(1) from repos %s`, whereClause) 461 var count int64 462 err := e.QueryRow(repoQuery, args...).Scan(&count) 463 464 if !errors.Is(err, sql.ErrNoRows) && err != nil { 465 return 0, err 466 } 467 468 return count, nil 469} 470 471func GetRepoByAtUri(e Execer, atUri string) (*models.Repo, error) { 472 var repo models.Repo 473 var nullableDescription sql.NullString 474 var nullableWebsite sql.NullString 475 var nullableTopicStr sql.NullString 476 var nullableRepoDid sql.NullString 477 var nullableSource sql.NullString 478 var nullableSpindle sql.NullString 479 480 row := e.QueryRow(`select id, did, name, knot, created, rkey, description, website, topics, source, spindle, repo_did from repos where at_uri = ?`, atUri) 481 482 var createdAt string 483 if err := row.Scan(&repo.Id, &repo.Did, &repo.Name, &repo.Knot, &createdAt, &repo.Rkey, &nullableDescription, &nullableWebsite, &nullableTopicStr, &nullableSource, &nullableSpindle, &nullableRepoDid); err != nil { 484 return nil, err 485 } 486 createdAtTime, _ := time.Parse(time.RFC3339, createdAt) 487 repo.Created = createdAtTime 488 489 if nullableDescription.Valid { 490 repo.Description = nullableDescription.String 491 } 492 if nullableWebsite.Valid { 493 repo.Website = nullableWebsite.String 494 } 495 if nullableTopicStr.Valid { 496 repo.Topics = strings.Fields(nullableTopicStr.String) 497 } 498 if nullableSource.Valid { 499 repo.Source = nullableSource.String 500 } 501 if nullableSpindle.Valid { 502 repo.Spindle = nullableSpindle.String 503 } 504 if nullableRepoDid.Valid { 505 repo.RepoDid = nullableRepoDid.String 506 } 507 508 return &repo, nil 509} 510 511func PutRepo(tx *sql.Tx, repo models.Repo) error { 512 var repoDid *string 513 if repo.RepoDid != "" { 514 repoDid = &repo.RepoDid 515 } 516 _, err := tx.Exec( 517 `update repos 518 set name = ?, knot = ?, description = ?, website = ?, topics = ?, repo_did = coalesce(?, repo_did) 519 where did = ? and rkey = ? 520 `, 521 repo.Name, repo.Knot, repo.Description, repo.Website, repo.TopicStr(), repoDid, repo.Did, repo.Rkey, 522 ) 523 return err 524} 525 526func AddRepo(tx *sql.Tx, repo *models.Repo) error { 527 var repoDid *string 528 if repo.RepoDid != "" { 529 repoDid = &repo.RepoDid 530 } 531 result, err := tx.Exec( 532 `insert into repos 533 (did, name, knot, rkey, at_uri, description, website, topics, source, repo_did) 534 values (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, 535 repo.Did, repo.Name, repo.Knot, repo.Rkey, repo.RepoAt().String(), repo.Description, repo.Website, repo.TopicStr(), repo.Source, repoDid, 536 ) 537 if err != nil { 538 return fmt.Errorf("failed to insert repo: %w", err) 539 } 540 541 id, err := result.LastInsertId() 542 if err != nil { 543 return fmt.Errorf("failed to get last insert id: %w", err) 544 } 545 repo.Id = id 546 547 for _, dl := range repo.Labels { 548 if err := SubscribeLabel(tx, &models.RepoLabel{ 549 RepoDid: syntax.DID(repo.RepoDid), 550 LabelAt: syntax.ATURI(dl), 551 }); err != nil { 552 return fmt.Errorf("failed to subscribe to label: %w", err) 553 } 554 } 555 556 return nil 557} 558 559func RemoveRepo(e Execer, did, rkey string) error { 560 _, err := e.Exec(`delete from repos where did = ? and rkey = ?`, did, rkey) 561 return err 562} 563 564func RemoveReposByKnot(e Execer, knot string) error { 565 _, err := e.Exec(`delete from repos where knot = ?`, knot) 566 return err 567} 568 569func GetRepoSource(e Execer, repoDid string) (string, error) { 570 var nullableSource sql.NullString 571 err := e.QueryRow(`select source from repos where repo_did = ?`, repoDid).Scan(&nullableSource) 572 if err != nil { 573 return "", err 574 } 575 return nullableSource.String, nil 576} 577 578func GetRepoSourceRepo(e Execer, repoDid string) (*models.Repo, error) { 579 source, err := GetRepoSource(e, repoDid) 580 if source == "" || errors.Is(err, sql.ErrNoRows) { 581 return nil, nil 582 } 583 if err != nil { 584 return nil, err 585 } 586 if strings.HasPrefix(source, "did:") { 587 return GetRepoByDid(e, source) 588 } 589 return GetRepoByAtUri(e, source) 590} 591 592func GetForksByDid(e Execer, did string) ([]models.Repo, error) { 593 var repos []models.Repo 594 595 rows, err := e.Query( 596 `select distinct r.id, r.did, r.name, r.knot, r.rkey, r.description, r.website, r.created, r.source, r.repo_did 597 from repos r 598 left join collaborators c on r.repo_did = c.repo_did 599 where (r.did = ? or c.subject_did = ?) 600 and r.source is not null 601 and r.source != '' 602 order by r.created desc`, 603 did, did, 604 ) 605 if err != nil { 606 return nil, err 607 } 608 defer rows.Close() 609 610 for rows.Next() { 611 var repo models.Repo 612 var createdAt string 613 var nullableDescription sql.NullString 614 var nullableWebsite sql.NullString 615 var nullableSource sql.NullString 616 var nullableRepoDid sql.NullString 617 618 err := rows.Scan(&repo.Id, &repo.Did, &repo.Name, &repo.Knot, &repo.Rkey, &nullableDescription, &nullableWebsite, &createdAt, &nullableSource, &nullableRepoDid) 619 if err != nil { 620 return nil, err 621 } 622 623 if nullableDescription.Valid { 624 repo.Description = nullableDescription.String 625 } 626 if nullableWebsite.Valid { 627 repo.Website = nullableWebsite.String 628 } 629 630 if nullableSource.Valid { 631 repo.Source = nullableSource.String 632 } 633 if nullableRepoDid.Valid { 634 repo.RepoDid = nullableRepoDid.String 635 } 636 637 createdAtTime, err := time.Parse(time.RFC3339, createdAt) 638 if err != nil { 639 repo.Created = time.Now() 640 } else { 641 repo.Created = createdAtTime 642 } 643 644 repos = append(repos, repo) 645 } 646 647 if err := rows.Err(); err != nil { 648 return nil, err 649 } 650 651 return repos, nil 652} 653 654func GetRepoByDid(e Execer, repoDid string) (*models.Repo, error) { 655 return GetRepo(e, orm.FilterEq("repo_did", repoDid)) 656} 657 658func GetForkByRepoDid(e Execer, repoDid string) (*models.Repo, error) { 659 return GetRepo(e, orm.FilterEq("repo_did", repoDid), orm.FilterNotEq("source", "")) 660} 661 662// TODO: just queue every legacy records regardless of target repo has a DID or not. 663// doable after we have `repo_did` column in db for each tables. 664func EnqueuePdsRewritesForRepo(tx *sql.Tx, repoDid, repoAtUri string) error { 665 type record struct { 666 userDidCol string 667 table string 668 nsid syntax.NSID 669 fkCol string 670 fkVal string 671 } 672 sources := []record{ 673 {"did", "repos", tangled.RepoNSID, "at_uri", repoAtUri}, 674 {"did", "issues", tangled.RepoIssueNSID, "repo_did", repoDid}, 675 {"owner_did", "pulls", tangled.RepoPullNSID, "repo_did", repoDid}, 676 {"did", "collaborators", tangled.RepoCollaboratorNSID, "repo_did", repoDid}, 677 {"did", "artifacts", tangled.RepoArchiveNSID, "repo_did", repoDid}, 678 {"did", "stars", tangled.FeedStarNSID, "subject", repoDid}, 679 } 680 681 for _, src := range sources { 682 rows, err := tx.Query( 683 fmt.Sprintf(`SELECT %s, rkey FROM %s WHERE %s = ?`, src.userDidCol, src.table, src.fkCol), 684 src.fkVal, 685 ) 686 if err != nil { 687 return fmt.Errorf("query %s for pds rewrites: %w", src.table, err) 688 } 689 690 var pairs []struct{ did, rkey string } 691 for rows.Next() { 692 var d string 693 var r sql.NullString 694 if scanErr := rows.Scan(&d, &r); scanErr != nil { 695 rows.Close() 696 return fmt.Errorf("scan %s for pds rewrites: %w", src.table, scanErr) 697 } 698 if !r.Valid { 699 continue 700 } 701 pairs = append(pairs, struct{ did, rkey string }{d, r.String}) 702 } 703 rows.Close() 704 if rowsErr := rows.Err(); rowsErr != nil { 705 return fmt.Errorf("iterate %s for pds rewrites: %w", src.table, rowsErr) 706 } 707 708 for _, p := range pairs { 709 if err := EnqueuePdsRecordMigration(context.Background(), tx, "add-repo-did", syntax.DID(p.did), src.nsid, syntax.RecordKey(p.rkey)); err != nil { 710 return fmt.Errorf("enqueue pds rewrite for %s/%s: %w", src.table, p.rkey, err) 711 } 712 } 713 } 714 715 profileRows, err := tx.Query( 716 `SELECT DISTINCT did FROM profile_pinned_repositories WHERE pin = ?`, 717 repoAtUri, 718 ) 719 if err != nil { 720 return fmt.Errorf("query profile_pinned_repositories for pds rewrites: %w", err) 721 } 722 var profileDids []string 723 for profileRows.Next() { 724 var d string 725 if scanErr := profileRows.Scan(&d); scanErr != nil { 726 profileRows.Close() 727 return fmt.Errorf("scan profile_pinned_repositories for pds rewrites: %w", scanErr) 728 } 729 profileDids = append(profileDids, d) 730 } 731 profileRows.Close() 732 if profileRowsErr := profileRows.Err(); profileRowsErr != nil { 733 return fmt.Errorf("iterate profile_pinned_repositories for pds rewrites: %w", profileRowsErr) 734 } 735 736 for _, d := range profileDids { 737 if err := EnqueuePdsRecordMigration(context.Background(), tx, "add-repo-did", syntax.DID(d), tangled.ActorProfileNSID, "self"); err != nil { 738 return fmt.Errorf("enqueue pds rewrite for profile/%s: %w", d, err) 739 } 740 } 741 742 return nil 743} 744 745func CascadeRepoDid(tx *sql.Tx, repoAtUri, repoDid string) error { 746 _, err := tx.Exec( 747 `UPDATE repos SET repo_did = ? WHERE at_uri = ?`, 748 repoDid, repoAtUri, 749 ) 750 if err != nil { 751 return fmt.Errorf("cascade repo_did to repos: %w", err) 752 } 753 754 _, err = tx.Exec( 755 `UPDATE repos SET source = ? WHERE source = ?`, 756 repoDid, repoAtUri, 757 ) 758 if err != nil { 759 return fmt.Errorf("cascade repo_did to repos.source: %w", err) 760 } 761 762 return nil 763} 764 765func UpdateDescription(e Execer, repoDid, newDescription string) error { 766 _, err := e.Exec( 767 `update repos set description = ? where repo_did = ?`, newDescription, repoDid) 768 return err 769} 770 771func UpdateSpindle(e Execer, repoDid string, spindle *string) error { 772 _, err := e.Exec( 773 `update repos set spindle = ? where repo_did = ?`, spindle, repoDid) 774 return err 775} 776 777func SubscribeLabel(e Execer, rl *models.RepoLabel) error { 778 query := `insert or ignore into repo_labels (repo_did, label_at) values (?, ?)` 779 780 _, err := e.Exec(query, string(rl.RepoDid), rl.LabelAt.String()) 781 return err 782} 783 784func UnsubscribeLabel(e Execer, filters ...orm.Filter) error { 785 var conditions []string 786 var args []any 787 for _, filter := range filters { 788 conditions = append(conditions, filter.Condition()) 789 args = append(args, filter.Arg()...) 790 } 791 792 whereClause := "" 793 if conditions != nil { 794 whereClause = " where " + strings.Join(conditions, " and ") 795 } 796 797 query := fmt.Sprintf(`delete from repo_labels %s`, whereClause) 798 _, err := e.Exec(query, args...) 799 return err 800} 801 802func GetRepoLabels(e Execer, filters ...orm.Filter) ([]models.RepoLabel, error) { 803 var conditions []string 804 var args []any 805 for _, filter := range filters { 806 conditions = append(conditions, filter.Condition()) 807 args = append(args, filter.Arg()...) 808 } 809 810 whereClause := "" 811 if conditions != nil { 812 whereClause = " where " + strings.Join(conditions, " and ") 813 } 814 815 query := fmt.Sprintf(`select id, repo_did, label_at from repo_labels %s`, whereClause) 816 817 rows, err := e.Query(query, args...) 818 if err != nil { 819 return nil, err 820 } 821 defer rows.Close() 822 823 var labels []models.RepoLabel 824 for rows.Next() { 825 var label models.RepoLabel 826 827 err := rows.Scan(&label.Id, &label.RepoDid, &label.LabelAt) 828 if err != nil { 829 return nil, err 830 } 831 832 labels = append(labels, label) 833 } 834 835 if err = rows.Err(); err != nil { 836 return nil, err 837 } 838 839 return labels, nil 840} 841 842func GetForkCount(e Execer, sourceDID string) (int, error) { 843 forks := 0 844 err := e.QueryRow( 845 `select count(source) from repos where source = ?`, sourceDID).Scan(&forks) 846 if err != nil { 847 return 0, err 848 } 849 return forks, nil 850}