forked from tangled.org/core
Monorepo for Tangled — https://tangled.org
at ci 13 kB view raw
1package db 2 3import ( 4 "database/sql" 5 "fmt" 6 "log" 7 "slices" 8 "strings" 9 "time" 10 11 "github.com/bluesky-social/indigo/atproto/syntax" 12 securejoin "github.com/cyphar/filepath-securejoin" 13 "tangled.sh/tangled.sh/core/api/tangled" 14) 15 16type Repo struct { 17 Did string 18 Name string 19 Knot string 20 Rkey string 21 Created time.Time 22 AtUri string 23 Description string 24 Spindle string 25 26 // optionally, populate this when querying for reverse mappings 27 RepoStats *RepoStats 28 29 // optional 30 Source string 31} 32 33func (r Repo) RepoAt() syntax.ATURI { 34 return syntax.ATURI(fmt.Sprintf("at://%s/%s/%s", r.Did, tangled.RepoNSID, r.Rkey)) 35} 36 37func (r Repo) DidSlashRepo() string { 38 p, _ := securejoin.SecureJoin(r.Did, r.Name) 39 return p 40} 41 42func GetAllRepos(e Execer, limit int) ([]Repo, error) { 43 var repos []Repo 44 45 rows, err := e.Query( 46 `select did, name, knot, rkey, description, created, source 47 from repos 48 order by created desc 49 limit ? 50 `, 51 limit, 52 ) 53 if err != nil { 54 return nil, err 55 } 56 defer rows.Close() 57 58 for rows.Next() { 59 var repo Repo 60 err := scanRepo( 61 rows, &repo.Did, &repo.Name, &repo.Knot, &repo.Rkey, &repo.Description, &repo.Created, &repo.Source, 62 ) 63 if err != nil { 64 return nil, err 65 } 66 repos = append(repos, repo) 67 } 68 69 if err := rows.Err(); err != nil { 70 return nil, err 71 } 72 73 return repos, nil 74} 75 76func GetRepos(e Execer, limit int, filters ...filter) ([]Repo, error) { 77 repoMap := make(map[syntax.ATURI]*Repo) 78 79 var conditions []string 80 var args []any 81 for _, filter := range filters { 82 conditions = append(conditions, filter.Condition()) 83 args = append(args, filter.Arg()...) 84 } 85 86 whereClause := "" 87 if conditions != nil { 88 whereClause = " where " + strings.Join(conditions, " and ") 89 } 90 91 limitClause := "" 92 if limit != 0 { 93 limitClause = fmt.Sprintf(" limit %d", limit) 94 } 95 96 repoQuery := fmt.Sprintf( 97 `select 98 did, 99 name, 100 knot, 101 rkey, 102 created, 103 description, 104 source, 105 spindle 106 from 107 repos r 108 %s 109 order by created desc 110 %s`, 111 whereClause, 112 limitClause, 113 ) 114 rows, err := e.Query(repoQuery, args...) 115 116 if err != nil { 117 return nil, fmt.Errorf("failed to execute repo query: %w ", err) 118 } 119 120 for rows.Next() { 121 var repo Repo 122 var createdAt string 123 var description, source, spindle sql.NullString 124 125 err := rows.Scan( 126 &repo.Did, 127 &repo.Name, 128 &repo.Knot, 129 &repo.Rkey, 130 &createdAt, 131 &description, 132 &source, 133 &spindle, 134 ) 135 if err != nil { 136 return nil, fmt.Errorf("failed to execute repo query: %w ", err) 137 } 138 139 if t, err := time.Parse(time.RFC3339, createdAt); err == nil { 140 repo.Created = t 141 } 142 if description.Valid { 143 repo.Description = description.String 144 } 145 if source.Valid { 146 repo.Source = source.String 147 } 148 if spindle.Valid { 149 repo.Spindle = spindle.String 150 } 151 152 repo.RepoStats = &RepoStats{} 153 repoMap[repo.RepoAt()] = &repo 154 } 155 156 if err = rows.Err(); err != nil { 157 return nil, fmt.Errorf("failed to execute repo query: %w ", err) 158 } 159 160 inClause := strings.TrimSuffix(strings.Repeat("?, ", len(repoMap)), ", ") 161 args = make([]any, len(repoMap)) 162 163 i := 0 164 for _, r := range repoMap { 165 args[i] = r.RepoAt() 166 i++ 167 } 168 169 languageQuery := fmt.Sprintf( 170 ` 171 select 172 repo_at, language 173 from 174 repo_languages r1 175 where 176 repo_at IN (%s) 177 and is_default_ref = 1 178 and id = ( 179 select id 180 from repo_languages r2 181 where r2.repo_at = r1.repo_at 182 and r2.is_default_ref = 1 183 order by bytes desc 184 limit 1 185 ); 186 `, 187 inClause, 188 ) 189 rows, err = e.Query(languageQuery, args...) 190 if err != nil { 191 return nil, fmt.Errorf("failed to execute lang query: %w ", err) 192 } 193 for rows.Next() { 194 var repoat, lang string 195 if err := rows.Scan(&repoat, &lang); err != nil { 196 log.Println("err", "err", err) 197 continue 198 } 199 if r, ok := repoMap[syntax.ATURI(repoat)]; ok { 200 r.RepoStats.Language = lang 201 } 202 } 203 if err = rows.Err(); err != nil { 204 return nil, fmt.Errorf("failed to execute lang query: %w ", err) 205 } 206 207 starCountQuery := fmt.Sprintf( 208 `select 209 repo_at, count(1) 210 from stars 211 where repo_at in (%s) 212 group by repo_at`, 213 inClause, 214 ) 215 rows, err = e.Query(starCountQuery, args...) 216 if err != nil { 217 return nil, fmt.Errorf("failed to execute star-count query: %w ", err) 218 } 219 for rows.Next() { 220 var repoat string 221 var count int 222 if err := rows.Scan(&repoat, &count); err != nil { 223 log.Println("err", "err", err) 224 continue 225 } 226 if r, ok := repoMap[syntax.ATURI(repoat)]; ok { 227 r.RepoStats.StarCount = count 228 } 229 } 230 if err = rows.Err(); err != nil { 231 return nil, fmt.Errorf("failed to execute star-count query: %w ", err) 232 } 233 234 issueCountQuery := fmt.Sprintf( 235 `select 236 repo_at, 237 count(case when open = 1 then 1 end) as open_count, 238 count(case when open = 0 then 1 end) as closed_count 239 from issues 240 where repo_at in (%s) 241 group by repo_at`, 242 inClause, 243 ) 244 rows, err = e.Query(issueCountQuery, args...) 245 if err != nil { 246 return nil, fmt.Errorf("failed to execute issue-count query: %w ", err) 247 } 248 for rows.Next() { 249 var repoat string 250 var open, closed int 251 if err := rows.Scan(&repoat, &open, &closed); err != nil { 252 log.Println("err", "err", err) 253 continue 254 } 255 if r, ok := repoMap[syntax.ATURI(repoat)]; ok { 256 r.RepoStats.IssueCount.Open = open 257 r.RepoStats.IssueCount.Closed = closed 258 } 259 } 260 if err = rows.Err(); err != nil { 261 return nil, fmt.Errorf("failed to execute issue-count query: %w ", err) 262 } 263 264 pullCountQuery := fmt.Sprintf( 265 `select 266 repo_at, 267 count(case when state = ? then 1 end) as open_count, 268 count(case when state = ? then 1 end) as merged_count, 269 count(case when state = ? then 1 end) as closed_count, 270 count(case when state = ? then 1 end) as deleted_count 271 from pulls 272 where repo_at in (%s) 273 group by repo_at`, 274 inClause, 275 ) 276 args = append([]any{ 277 PullOpen, 278 PullMerged, 279 PullClosed, 280 PullDeleted, 281 }, args...) 282 rows, err = e.Query( 283 pullCountQuery, 284 args..., 285 ) 286 if err != nil { 287 return nil, fmt.Errorf("failed to execute pulls-count query: %w ", err) 288 } 289 for rows.Next() { 290 var repoat string 291 var open, merged, closed, deleted int 292 if err := rows.Scan(&repoat, &open, &merged, &closed, &deleted); err != nil { 293 log.Println("err", "err", err) 294 continue 295 } 296 if r, ok := repoMap[syntax.ATURI(repoat)]; ok { 297 r.RepoStats.PullCount.Open = open 298 r.RepoStats.PullCount.Merged = merged 299 r.RepoStats.PullCount.Closed = closed 300 r.RepoStats.PullCount.Deleted = deleted 301 } 302 } 303 if err = rows.Err(); err != nil { 304 return nil, fmt.Errorf("failed to execute pulls-count query: %w ", err) 305 } 306 307 var repos []Repo 308 for _, r := range repoMap { 309 repos = append(repos, *r) 310 } 311 312 slices.SortFunc(repos, func(a, b Repo) int { 313 if a.Created.After(b.Created) { 314 return 1 315 } 316 return -1 317 }) 318 319 return repos, nil 320} 321 322func GetAllReposByDid(e Execer, did string) ([]Repo, error) { 323 var repos []Repo 324 325 rows, err := e.Query( 326 `select 327 r.did, 328 r.name, 329 r.knot, 330 r.rkey, 331 r.description, 332 r.created, 333 count(s.id) as star_count, 334 r.source 335 from 336 repos r 337 left join 338 stars s on r.at_uri = s.repo_at 339 where 340 r.did = ? 341 group by 342 r.at_uri 343 order by r.created desc`, 344 did) 345 if err != nil { 346 return nil, err 347 } 348 defer rows.Close() 349 350 for rows.Next() { 351 var repo Repo 352 var repoStats RepoStats 353 var createdAt string 354 var nullableDescription sql.NullString 355 var nullableSource sql.NullString 356 357 err := rows.Scan(&repo.Did, &repo.Name, &repo.Knot, &repo.Rkey, &nullableDescription, &createdAt, &repoStats.StarCount, &nullableSource) 358 if err != nil { 359 return nil, err 360 } 361 362 if nullableDescription.Valid { 363 repo.Description = nullableDescription.String 364 } 365 366 if nullableSource.Valid { 367 repo.Source = nullableSource.String 368 } 369 370 createdAtTime, err := time.Parse(time.RFC3339, createdAt) 371 if err != nil { 372 repo.Created = time.Now() 373 } else { 374 repo.Created = createdAtTime 375 } 376 377 repo.RepoStats = &repoStats 378 379 repos = append(repos, repo) 380 } 381 382 if err := rows.Err(); err != nil { 383 return nil, err 384 } 385 386 return repos, nil 387} 388 389func GetRepo(e Execer, did, name string) (*Repo, error) { 390 var repo Repo 391 var description, spindle sql.NullString 392 393 row := e.QueryRow(` 394 select did, name, knot, created, at_uri, description, spindle 395 from repos 396 where did = ? and name = ? 397 `, 398 did, 399 name, 400 ) 401 402 var createdAt string 403 if err := row.Scan(&repo.Did, &repo.Name, &repo.Knot, &createdAt, &repo.AtUri, &description, &spindle); err != nil { 404 return nil, err 405 } 406 createdAtTime, _ := time.Parse(time.RFC3339, createdAt) 407 repo.Created = createdAtTime 408 409 if description.Valid { 410 repo.Description = description.String 411 } 412 413 if spindle.Valid { 414 repo.Spindle = spindle.String 415 } 416 417 return &repo, nil 418} 419 420func GetRepoByAtUri(e Execer, atUri string) (*Repo, error) { 421 var repo Repo 422 var nullableDescription sql.NullString 423 424 row := e.QueryRow(`select did, name, knot, created, at_uri, description from repos where at_uri = ?`, atUri) 425 426 var createdAt string 427 if err := row.Scan(&repo.Did, &repo.Name, &repo.Knot, &createdAt, &repo.AtUri, &nullableDescription); err != nil { 428 return nil, err 429 } 430 createdAtTime, _ := time.Parse(time.RFC3339, createdAt) 431 repo.Created = createdAtTime 432 433 if nullableDescription.Valid { 434 repo.Description = nullableDescription.String 435 } else { 436 repo.Description = "" 437 } 438 439 return &repo, nil 440} 441 442func AddRepo(e Execer, repo *Repo) error { 443 _, err := e.Exec( 444 `insert into repos 445 (did, name, knot, rkey, at_uri, description, source) 446 values (?, ?, ?, ?, ?, ?, ?)`, 447 repo.Did, repo.Name, repo.Knot, repo.Rkey, repo.AtUri, repo.Description, repo.Source, 448 ) 449 return err 450} 451 452func RemoveRepo(e Execer, did, name string) error { 453 _, err := e.Exec(`delete from repos where did = ? and name = ?`, did, name) 454 return err 455} 456 457func GetRepoSource(e Execer, repoAt syntax.ATURI) (string, error) { 458 var nullableSource sql.NullString 459 err := e.QueryRow(`select source from repos where at_uri = ?`, repoAt).Scan(&nullableSource) 460 if err != nil { 461 return "", err 462 } 463 return nullableSource.String, nil 464} 465 466func GetForksByDid(e Execer, did string) ([]Repo, error) { 467 var repos []Repo 468 469 rows, err := e.Query( 470 `select did, name, knot, rkey, description, created, at_uri, source 471 from repos 472 where did = ? and source is not null and source != '' 473 order by created desc`, 474 did, 475 ) 476 if err != nil { 477 return nil, err 478 } 479 defer rows.Close() 480 481 for rows.Next() { 482 var repo Repo 483 var createdAt string 484 var nullableDescription sql.NullString 485 var nullableSource sql.NullString 486 487 err := rows.Scan(&repo.Did, &repo.Name, &repo.Knot, &repo.Rkey, &nullableDescription, &createdAt, &repo.AtUri, &nullableSource) 488 if err != nil { 489 return nil, err 490 } 491 492 if nullableDescription.Valid { 493 repo.Description = nullableDescription.String 494 } 495 496 if nullableSource.Valid { 497 repo.Source = nullableSource.String 498 } 499 500 createdAtTime, err := time.Parse(time.RFC3339, createdAt) 501 if err != nil { 502 repo.Created = time.Now() 503 } else { 504 repo.Created = createdAtTime 505 } 506 507 repos = append(repos, repo) 508 } 509 510 if err := rows.Err(); err != nil { 511 return nil, err 512 } 513 514 return repos, nil 515} 516 517func GetForkByDid(e Execer, did string, name string) (*Repo, error) { 518 var repo Repo 519 var createdAt string 520 var nullableDescription sql.NullString 521 var nullableSource sql.NullString 522 523 row := e.QueryRow( 524 `select did, name, knot, rkey, description, created, at_uri, source 525 from repos 526 where did = ? and name = ? and source is not null and source != ''`, 527 did, name, 528 ) 529 530 err := row.Scan(&repo.Did, &repo.Name, &repo.Knot, &repo.Rkey, &nullableDescription, &createdAt, &repo.AtUri, &nullableSource) 531 if err != nil { 532 return nil, err 533 } 534 535 if nullableDescription.Valid { 536 repo.Description = nullableDescription.String 537 } 538 539 if nullableSource.Valid { 540 repo.Source = nullableSource.String 541 } 542 543 createdAtTime, err := time.Parse(time.RFC3339, createdAt) 544 if err != nil { 545 repo.Created = time.Now() 546 } else { 547 repo.Created = createdAtTime 548 } 549 550 return &repo, nil 551} 552 553func UpdateDescription(e Execer, repoAt, newDescription string) error { 554 _, err := e.Exec( 555 `update repos set description = ? where at_uri = ?`, newDescription, repoAt) 556 return err 557} 558 559func UpdateSpindle(e Execer, repoAt, spindle string) error { 560 _, err := e.Exec( 561 `update repos set spindle = ? where at_uri = ?`, spindle, repoAt) 562 return err 563} 564 565type RepoStats struct { 566 Language string 567 StarCount int 568 IssueCount IssueCount 569 PullCount PullCount 570} 571 572func scanRepo(rows *sql.Rows, did, name, knot, rkey, description *string, created *time.Time, source *string) error { 573 var createdAt string 574 var nullableDescription sql.NullString 575 var nullableSource sql.NullString 576 if err := rows.Scan(did, name, knot, rkey, &nullableDescription, &createdAt, &nullableSource); err != nil { 577 return err 578 } 579 580 if nullableDescription.Valid { 581 *description = nullableDescription.String 582 } else { 583 *description = "" 584 } 585 586 createdAtTime, err := time.Parse(time.RFC3339, createdAt) 587 if err != nil { 588 *created = time.Now() 589 } else { 590 *created = createdAtTime 591 } 592 593 if nullableSource.Valid { 594 *source = nullableSource.String 595 } else { 596 *source = "" 597 } 598 599 return nil 600}