forked from tangled.org/core
Monorepo for Tangled — https://tangled.org
1package db 2 3import ( 4 "context" 5 "database/sql" 6 "time" 7 8 "github.com/bluesky-social/indigo/atproto/syntax" 9 "go.opentelemetry.io/otel" 10 "go.opentelemetry.io/otel/attribute" 11) 12 13type Repo struct { 14 Did string 15 Name string 16 Knot string 17 Rkey string 18 Created time.Time 19 AtUri string 20 Description string 21 22 // optionally, populate this when querying for reverse mappings 23 RepoStats *RepoStats 24 25 // optional 26 Source string 27} 28 29func GetAllRepos(ctx context.Context, e Execer, limit int) ([]Repo, error) { 30 ctx, span := otel.Tracer("db").Start(ctx, "GetAllRepos") 31 defer span.End() 32 span.SetAttributes(attribute.Int("limit", limit)) 33 34 var repos []Repo 35 36 rows, err := e.Query( 37 `select did, name, knot, rkey, description, created, source 38 from repos 39 order by created desc 40 limit ? 41 `, 42 limit, 43 ) 44 if err != nil { 45 span.RecordError(err) 46 return nil, err 47 } 48 defer rows.Close() 49 50 for rows.Next() { 51 var repo Repo 52 err := scanRepo( 53 rows, &repo.Did, &repo.Name, &repo.Knot, &repo.Rkey, &repo.Description, &repo.Created, &repo.Source, 54 ) 55 if err != nil { 56 span.RecordError(err) 57 return nil, err 58 } 59 repos = append(repos, repo) 60 } 61 62 if err := rows.Err(); err != nil { 63 span.RecordError(err) 64 return nil, err 65 } 66 67 span.SetAttributes(attribute.Int("repos.count", len(repos))) 68 return repos, nil 69} 70 71func GetAllReposByDid(ctx context.Context, e Execer, did string) ([]Repo, error) { 72 ctx, span := otel.Tracer("db").Start(ctx, "GetAllReposByDid") 73 defer span.End() 74 span.SetAttributes(attribute.String("did", did)) 75 76 var repos []Repo 77 78 rows, err := e.Query( 79 `select 80 r.did, 81 r.name, 82 r.knot, 83 r.rkey, 84 r.description, 85 r.created, 86 count(s.id) as star_count, 87 r.source 88 from 89 repos r 90 left join 91 stars s on r.at_uri = s.repo_at 92 where 93 r.did = ? 94 group by 95 r.at_uri 96 order by r.created desc`, 97 did) 98 if err != nil { 99 span.RecordError(err) 100 return nil, err 101 } 102 defer rows.Close() 103 104 for rows.Next() { 105 var repo Repo 106 var repoStats RepoStats 107 var createdAt string 108 var nullableDescription sql.NullString 109 var nullableSource sql.NullString 110 111 err := rows.Scan(&repo.Did, &repo.Name, &repo.Knot, &repo.Rkey, &nullableDescription, &createdAt, &repoStats.StarCount, &nullableSource) 112 if err != nil { 113 span.RecordError(err) 114 return nil, err 115 } 116 117 if nullableDescription.Valid { 118 repo.Description = nullableDescription.String 119 } 120 121 if nullableSource.Valid { 122 repo.Source = nullableSource.String 123 } 124 125 createdAtTime, err := time.Parse(time.RFC3339, createdAt) 126 if err != nil { 127 repo.Created = time.Now() 128 } else { 129 repo.Created = createdAtTime 130 } 131 132 repo.RepoStats = &repoStats 133 134 repos = append(repos, repo) 135 } 136 137 if err := rows.Err(); err != nil { 138 span.RecordError(err) 139 return nil, err 140 } 141 142 span.SetAttributes(attribute.Int("repos.count", len(repos))) 143 return repos, nil 144} 145 146func GetRepo(ctx context.Context, e Execer, did, name string) (*Repo, error) { 147 ctx, span := otel.Tracer("db").Start(ctx, "GetRepo") 148 defer span.End() 149 span.SetAttributes( 150 attribute.String("did", did), 151 attribute.String("name", name), 152 ) 153 154 var repo Repo 155 var nullableDescription sql.NullString 156 157 row := e.QueryRow(`select did, name, knot, created, at_uri, description from repos where did = ? and name = ?`, did, name) 158 159 var createdAt string 160 if err := row.Scan(&repo.Did, &repo.Name, &repo.Knot, &createdAt, &repo.AtUri, &nullableDescription); err != nil { 161 span.RecordError(err) 162 return nil, err 163 } 164 createdAtTime, _ := time.Parse(time.RFC3339, createdAt) 165 repo.Created = createdAtTime 166 167 if nullableDescription.Valid { 168 repo.Description = nullableDescription.String 169 } else { 170 repo.Description = "" 171 } 172 173 return &repo, nil 174} 175 176func GetRepoByAtUri(ctx context.Context, e Execer, atUri string) (*Repo, error) { 177 ctx, span := otel.Tracer("db").Start(ctx, "GetRepoByAtUri") 178 defer span.End() 179 span.SetAttributes(attribute.String("atUri", atUri)) 180 181 var repo Repo 182 var nullableDescription sql.NullString 183 184 row := e.QueryRow(`select did, name, knot, created, at_uri, description from repos where at_uri = ?`, atUri) 185 186 var createdAt string 187 if err := row.Scan(&repo.Did, &repo.Name, &repo.Knot, &createdAt, &repo.AtUri, &nullableDescription); err != nil { 188 span.RecordError(err) 189 return nil, err 190 } 191 createdAtTime, _ := time.Parse(time.RFC3339, createdAt) 192 repo.Created = createdAtTime 193 194 if nullableDescription.Valid { 195 repo.Description = nullableDescription.String 196 } else { 197 repo.Description = "" 198 } 199 200 return &repo, nil 201} 202 203func AddRepo(ctx context.Context, e Execer, repo *Repo) error { 204 ctx, span := otel.Tracer("db").Start(ctx, "AddRepo") 205 defer span.End() 206 span.SetAttributes( 207 attribute.String("did", repo.Did), 208 attribute.String("name", repo.Name), 209 ) 210 211 _, err := e.Exec( 212 `insert into repos 213 (did, name, knot, rkey, at_uri, description, source) 214 values (?, ?, ?, ?, ?, ?, ?)`, 215 repo.Did, repo.Name, repo.Knot, repo.Rkey, repo.AtUri, repo.Description, repo.Source, 216 ) 217 if err != nil { 218 span.RecordError(err) 219 } 220 return err 221} 222 223func RemoveRepo(ctx context.Context, e Execer, did, name string) error { 224 ctx, span := otel.Tracer("db").Start(ctx, "RemoveRepo") 225 defer span.End() 226 span.SetAttributes( 227 attribute.String("did", did), 228 attribute.String("name", name), 229 ) 230 231 _, err := e.Exec(`delete from repos where did = ? and name = ?`, did, name) 232 if err != nil { 233 span.RecordError(err) 234 } 235 return err 236} 237 238func GetRepoSource(ctx context.Context, e Execer, repoAt syntax.ATURI) (string, error) { 239 ctx, span := otel.Tracer("db").Start(ctx, "GetRepoSource") 240 defer span.End() 241 span.SetAttributes(attribute.String("repoAt", repoAt.String())) 242 243 var nullableSource sql.NullString 244 err := e.QueryRow(`select source from repos where at_uri = ?`, repoAt).Scan(&nullableSource) 245 if err != nil { 246 span.RecordError(err) 247 return "", err 248 } 249 return nullableSource.String, nil 250} 251 252func GetForksByDid(ctx context.Context, e Execer, did string) ([]Repo, error) { 253 ctx, span := otel.Tracer("db").Start(ctx, "GetForksByDid") 254 defer span.End() 255 span.SetAttributes(attribute.String("did", did)) 256 257 var repos []Repo 258 259 rows, err := e.Query( 260 `select did, name, knot, rkey, description, created, at_uri, source 261 from repos 262 where did = ? and source is not null and source != '' 263 order by created desc`, 264 did, 265 ) 266 if err != nil { 267 span.RecordError(err) 268 return nil, err 269 } 270 defer rows.Close() 271 272 for rows.Next() { 273 var repo Repo 274 var createdAt string 275 var nullableDescription sql.NullString 276 var nullableSource sql.NullString 277 278 err := rows.Scan(&repo.Did, &repo.Name, &repo.Knot, &repo.Rkey, &nullableDescription, &createdAt, &repo.AtUri, &nullableSource) 279 if err != nil { 280 span.RecordError(err) 281 return nil, err 282 } 283 284 if nullableDescription.Valid { 285 repo.Description = nullableDescription.String 286 } 287 288 if nullableSource.Valid { 289 repo.Source = nullableSource.String 290 } 291 292 createdAtTime, err := time.Parse(time.RFC3339, createdAt) 293 if err != nil { 294 repo.Created = time.Now() 295 } else { 296 repo.Created = createdAtTime 297 } 298 299 repos = append(repos, repo) 300 } 301 302 if err := rows.Err(); err != nil { 303 span.RecordError(err) 304 return nil, err 305 } 306 307 span.SetAttributes(attribute.Int("forks.count", len(repos))) 308 return repos, nil 309} 310 311func GetForkByDid(ctx context.Context, e Execer, did string, name string) (*Repo, error) { 312 ctx, span := otel.Tracer("db").Start(ctx, "GetForkByDid") 313 defer span.End() 314 span.SetAttributes( 315 attribute.String("did", did), 316 attribute.String("name", name), 317 ) 318 319 var repo Repo 320 var createdAt string 321 var nullableDescription sql.NullString 322 var nullableSource sql.NullString 323 324 row := e.QueryRow( 325 `select did, name, knot, rkey, description, created, at_uri, source 326 from repos 327 where did = ? and name = ? and source is not null and source != ''`, 328 did, name, 329 ) 330 331 err := row.Scan(&repo.Did, &repo.Name, &repo.Knot, &repo.Rkey, &nullableDescription, &createdAt, &repo.AtUri, &nullableSource) 332 if err != nil { 333 span.RecordError(err) 334 return nil, err 335 } 336 337 if nullableDescription.Valid { 338 repo.Description = nullableDescription.String 339 } 340 341 if nullableSource.Valid { 342 repo.Source = nullableSource.String 343 } 344 345 createdAtTime, err := time.Parse(time.RFC3339, createdAt) 346 if err != nil { 347 repo.Created = time.Now() 348 } else { 349 repo.Created = createdAtTime 350 } 351 352 return &repo, nil 353} 354 355func AddCollaborator(ctx context.Context, e Execer, collaborator, repoOwnerDid, repoName, repoKnot string) error { 356 ctx, span := otel.Tracer("db").Start(ctx, "AddCollaborator") 357 defer span.End() 358 span.SetAttributes( 359 attribute.String("collaborator", collaborator), 360 attribute.String("repoOwnerDid", repoOwnerDid), 361 attribute.String("repoName", repoName), 362 ) 363 364 _, err := e.Exec( 365 `insert into collaborators (did, repo) 366 values (?, (select id from repos where did = ? and name = ? and knot = ?));`, 367 collaborator, repoOwnerDid, repoName, repoKnot) 368 if err != nil { 369 span.RecordError(err) 370 } 371 return err 372} 373 374func UpdateDescription(ctx context.Context, e Execer, repoAt, newDescription string) error { 375 ctx, span := otel.Tracer("db").Start(ctx, "UpdateDescription") 376 defer span.End() 377 span.SetAttributes( 378 attribute.String("repoAt", repoAt), 379 attribute.String("description", newDescription), 380 ) 381 382 _, err := e.Exec( 383 `update repos set description = ? where at_uri = ?`, newDescription, repoAt) 384 if err != nil { 385 span.RecordError(err) 386 } 387 return err 388} 389 390func CollaboratingIn(ctx context.Context, e Execer, collaborator string) ([]Repo, error) { 391 ctx, span := otel.Tracer("db").Start(ctx, "CollaboratingIn") 392 defer span.End() 393 span.SetAttributes(attribute.String("collaborator", collaborator)) 394 395 var repos []Repo 396 397 rows, err := e.Query( 398 `select 399 r.did, r.name, r.knot, r.rkey, r.description, r.created, count(s.id) as star_count 400 from 401 repos r 402 join 403 collaborators c on r.id = c.repo 404 left join 405 stars s on r.at_uri = s.repo_at 406 where 407 c.did = ? 408 group by 409 r.id;`, collaborator) 410 if err != nil { 411 span.RecordError(err) 412 return nil, err 413 } 414 defer rows.Close() 415 416 for rows.Next() { 417 var repo Repo 418 var repoStats RepoStats 419 var createdAt string 420 var nullableDescription sql.NullString 421 422 err := rows.Scan(&repo.Did, &repo.Name, &repo.Knot, &repo.Rkey, &nullableDescription, &createdAt, &repoStats.StarCount) 423 if err != nil { 424 span.RecordError(err) 425 return nil, err 426 } 427 428 if nullableDescription.Valid { 429 repo.Description = nullableDescription.String 430 } else { 431 repo.Description = "" 432 } 433 434 createdAtTime, err := time.Parse(time.RFC3339, createdAt) 435 if err != nil { 436 repo.Created = time.Now() 437 } else { 438 repo.Created = createdAtTime 439 } 440 441 repo.RepoStats = &repoStats 442 443 repos = append(repos, repo) 444 } 445 446 if err := rows.Err(); err != nil { 447 span.RecordError(err) 448 return nil, err 449 } 450 451 span.SetAttributes(attribute.Int("repos.count", len(repos))) 452 return repos, nil 453} 454 455type RepoStats struct { 456 StarCount int 457 IssueCount IssueCount 458 PullCount PullCount 459} 460 461func scanRepo(rows *sql.Rows, did, name, knot, rkey, description *string, created *time.Time, source *string) error { 462 var createdAt string 463 var nullableDescription sql.NullString 464 var nullableSource sql.NullString 465 if err := rows.Scan(did, name, knot, rkey, &nullableDescription, &createdAt, &nullableSource); err != nil { 466 return err 467 } 468 469 if nullableDescription.Valid { 470 *description = nullableDescription.String 471 } else { 472 *description = "" 473 } 474 475 createdAtTime, err := time.Parse(time.RFC3339, createdAt) 476 if err != nil { 477 *created = time.Now() 478 } else { 479 *created = createdAtTime 480 } 481 482 if nullableSource.Valid { 483 *source = nullableSource.String 484 } else { 485 *source = "" 486 } 487 488 return nil 489}