Monorepo for Tangled
1
fork

Configure Feed

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

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