Monorepo for Tangled
at master 487 lines 12 kB view raw
1package db 2 3import ( 4 "database/sql" 5 "fmt" 6 "strings" 7 8 "github.com/bluesky-social/indigo/atproto/syntax" 9 "tangled.org/core/api/tangled" 10 "tangled.org/core/appview/models" 11 "tangled.org/core/orm" 12) 13 14// ValidateReferenceLinks resolves refLinks to Issue/PR/IssueComment/PullComment ATURIs. 15// It will ignore missing refLinks. 16func ValidateReferenceLinks(e Execer, refLinks []models.ReferenceLink) ([]syntax.ATURI, error) { 17 var ( 18 issueRefs []models.ReferenceLink 19 pullRefs []models.ReferenceLink 20 ) 21 for _, ref := range refLinks { 22 switch ref.Kind { 23 case models.RefKindIssue: 24 issueRefs = append(issueRefs, ref) 25 case models.RefKindPull: 26 pullRefs = append(pullRefs, ref) 27 } 28 } 29 issueUris, err := findIssueReferences(e, issueRefs) 30 if err != nil { 31 return nil, fmt.Errorf("find issue references: %w", err) 32 } 33 pullUris, err := findPullReferences(e, pullRefs) 34 if err != nil { 35 return nil, fmt.Errorf("find pull references: %w", err) 36 } 37 38 return append(issueUris, pullUris...), nil 39} 40 41func findIssueReferences(e Execer, refLinks []models.ReferenceLink) ([]syntax.ATURI, error) { 42 if len(refLinks) == 0 { 43 return nil, nil 44 } 45 vals := make([]string, len(refLinks)) 46 args := make([]any, 0, len(refLinks)*4) 47 for i, ref := range refLinks { 48 vals[i] = "(?, ?, ?, ?)" 49 args = append(args, ref.Handle, ref.Repo, ref.SubjectId, ref.CommentId) 50 } 51 query := fmt.Sprintf( 52 `with input(owner_did, name, issue_id, comment_id) as ( 53 values %s 54 ) 55 select 56 i.did, i.rkey, 57 c.did, c.rkey 58 from input inp 59 join repos r 60 on r.did = inp.owner_did 61 and r.name = inp.name 62 join issues i 63 on coalesce( 64 nullif(i.repo_did, '') = nullif(r.repo_did, ''), 65 i.repo_at = r.at_uri 66 ) 67 and i.issue_id = inp.issue_id 68 left join issue_comments c 69 on inp.comment_id is not null 70 and c.issue_at = i.at_uri 71 and c.id = inp.comment_id 72 `, 73 strings.Join(vals, ","), 74 ) 75 rows, err := e.Query(query, args...) 76 if err != nil { 77 return nil, err 78 } 79 defer rows.Close() 80 81 var uris []syntax.ATURI 82 83 for rows.Next() { 84 // Scan rows 85 var issueOwner, issueRkey string 86 var commentOwner, commentRkey sql.NullString 87 var uri syntax.ATURI 88 if err := rows.Scan(&issueOwner, &issueRkey, &commentOwner, &commentRkey); err != nil { 89 return nil, err 90 } 91 if commentOwner.Valid && commentRkey.Valid { 92 uri = syntax.ATURI(fmt.Sprintf( 93 "at://%s/%s/%s", 94 commentOwner.String, 95 tangled.RepoIssueCommentNSID, 96 commentRkey.String, 97 )) 98 } else { 99 uri = syntax.ATURI(fmt.Sprintf( 100 "at://%s/%s/%s", 101 issueOwner, 102 tangled.RepoIssueNSID, 103 issueRkey, 104 )) 105 } 106 uris = append(uris, uri) 107 } 108 if err := rows.Err(); err != nil { 109 return nil, fmt.Errorf("iterate rows: %w", err) 110 } 111 112 return uris, nil 113} 114 115func findPullReferences(e Execer, refLinks []models.ReferenceLink) ([]syntax.ATURI, error) { 116 if len(refLinks) == 0 { 117 return nil, nil 118 } 119 vals := make([]string, len(refLinks)) 120 args := make([]any, 0, len(refLinks)*4) 121 for i, ref := range refLinks { 122 vals[i] = "(?, ?, ?, ?)" 123 args = append(args, ref.Handle, ref.Repo, ref.SubjectId, ref.CommentId) 124 } 125 query := fmt.Sprintf( 126 `with input(owner_did, name, pull_id, comment_id) as ( 127 values %s 128 ) 129 select 130 p.owner_did, p.rkey, 131 c.comment_at 132 from input inp 133 join repos r 134 on r.did = inp.owner_did 135 and r.name = inp.name 136 join pulls p 137 on coalesce( 138 nullif(p.repo_did, '') = nullif(r.repo_did, ''), 139 p.repo_at = r.at_uri 140 ) 141 and p.pull_id = inp.pull_id 142 left join pull_comments c 143 on inp.comment_id is not null 144 and coalesce( 145 nullif(c.repo_did, '') = nullif(r.repo_did, ''), 146 c.repo_at = r.at_uri 147 ) and c.pull_id = p.pull_id 148 and c.id = inp.comment_id 149 `, 150 strings.Join(vals, ","), 151 ) 152 rows, err := e.Query(query, args...) 153 if err != nil { 154 return nil, err 155 } 156 defer rows.Close() 157 158 var uris []syntax.ATURI 159 160 for rows.Next() { 161 // Scan rows 162 var pullOwner, pullRkey string 163 var commentUri sql.NullString 164 var uri syntax.ATURI 165 if err := rows.Scan(&pullOwner, &pullRkey, &commentUri); err != nil { 166 return nil, err 167 } 168 if commentUri.Valid { 169 // no-op 170 uri = syntax.ATURI(commentUri.String) 171 } else { 172 uri = syntax.ATURI(fmt.Sprintf( 173 "at://%s/%s/%s", 174 pullOwner, 175 tangled.RepoPullNSID, 176 pullRkey, 177 )) 178 } 179 uris = append(uris, uri) 180 } 181 return uris, nil 182} 183 184func putReferences(tx *sql.Tx, fromAt syntax.ATURI, references []syntax.ATURI) error { 185 err := deleteReferences(tx, fromAt) 186 if err != nil { 187 return fmt.Errorf("delete old reference_links: %w", err) 188 } 189 if len(references) == 0 { 190 return nil 191 } 192 193 values := make([]string, 0, len(references)) 194 args := make([]any, 0, len(references)*2) 195 for _, ref := range references { 196 values = append(values, "(?, ?)") 197 args = append(args, fromAt, ref) 198 } 199 _, err = tx.Exec( 200 fmt.Sprintf( 201 `insert into reference_links (from_at, to_at) 202 values %s`, 203 strings.Join(values, ","), 204 ), 205 args..., 206 ) 207 if err != nil { 208 return fmt.Errorf("insert new reference_links: %w", err) 209 } 210 return nil 211} 212 213func deleteReferences(tx *sql.Tx, fromAt syntax.ATURI) error { 214 _, err := tx.Exec(`delete from reference_links where from_at = ?`, fromAt) 215 return err 216} 217 218func GetReferencesAll(e Execer, filters ...orm.Filter) (map[syntax.ATURI][]syntax.ATURI, error) { 219 var ( 220 conditions []string 221 args []any 222 ) 223 for _, filter := range filters { 224 conditions = append(conditions, filter.Condition()) 225 args = append(args, filter.Arg()...) 226 } 227 228 whereClause := "" 229 if conditions != nil { 230 whereClause = " where " + strings.Join(conditions, " and ") 231 } 232 233 rows, err := e.Query( 234 fmt.Sprintf( 235 `select from_at, to_at from reference_links %s`, 236 whereClause, 237 ), 238 args..., 239 ) 240 if err != nil { 241 return nil, fmt.Errorf("query reference_links: %w", err) 242 } 243 defer rows.Close() 244 245 result := make(map[syntax.ATURI][]syntax.ATURI) 246 247 for rows.Next() { 248 var from, to syntax.ATURI 249 if err := rows.Scan(&from, &to); err != nil { 250 return nil, fmt.Errorf("scan row: %w", err) 251 } 252 253 result[from] = append(result[from], to) 254 } 255 if err := rows.Err(); err != nil { 256 return nil, fmt.Errorf("iterate rows: %w", err) 257 } 258 259 return result, nil 260} 261 262func GetBacklinks(e Execer, target syntax.ATURI) ([]models.RichReferenceLink, error) { 263 rows, err := e.Query( 264 `select from_at from reference_links 265 where to_at = ?`, 266 target, 267 ) 268 if err != nil { 269 return nil, fmt.Errorf("query backlinks: %w", err) 270 } 271 defer rows.Close() 272 273 var ( 274 backlinks []models.RichReferenceLink 275 backlinksMap = make(map[string][]syntax.ATURI) 276 ) 277 for rows.Next() { 278 var from syntax.ATURI 279 if err := rows.Scan(&from); err != nil { 280 return nil, fmt.Errorf("scan row: %w", err) 281 } 282 nsid := from.Collection().String() 283 backlinksMap[nsid] = append(backlinksMap[nsid], from) 284 } 285 if err := rows.Err(); err != nil { 286 return nil, fmt.Errorf("iterate rows: %w", err) 287 } 288 289 var ls []models.RichReferenceLink 290 ls, err = getIssueBacklinks(e, backlinksMap[tangled.RepoIssueNSID]) 291 if err != nil { 292 return nil, fmt.Errorf("get issue backlinks: %w", err) 293 } 294 backlinks = append(backlinks, ls...) 295 ls, err = getIssueCommentBacklinks(e, backlinksMap[tangled.RepoIssueCommentNSID]) 296 if err != nil { 297 return nil, fmt.Errorf("get issue_comment backlinks: %w", err) 298 } 299 backlinks = append(backlinks, ls...) 300 ls, err = getPullBacklinks(e, backlinksMap[tangled.RepoPullNSID]) 301 if err != nil { 302 return nil, fmt.Errorf("get pull backlinks: %w", err) 303 } 304 backlinks = append(backlinks, ls...) 305 ls, err = getPullCommentBacklinks(e, backlinksMap[tangled.RepoPullCommentNSID]) 306 if err != nil { 307 return nil, fmt.Errorf("get pull_comment backlinks: %w", err) 308 } 309 backlinks = append(backlinks, ls...) 310 311 return backlinks, nil 312} 313 314func getIssueBacklinks(e Execer, aturis []syntax.ATURI) ([]models.RichReferenceLink, error) { 315 if len(aturis) == 0 { 316 return nil, nil 317 } 318 vals := make([]string, len(aturis)) 319 args := make([]any, 0, len(aturis)*2) 320 for i, aturi := range aturis { 321 vals[i] = "(?, ?)" 322 did := aturi.Authority().String() 323 rkey := aturi.RecordKey().String() 324 args = append(args, did, rkey) 325 } 326 rows, err := e.Query( 327 fmt.Sprintf( 328 `select distinct r.did, r.name, i.issue_id, i.title, i.open 329 from issues i 330 join repos r 331 on coalesce( 332 nullif(i.repo_did, '') = nullif(r.repo_did, ''), 333 i.repo_at = r.at_uri 334 ) 335 where (i.did, i.rkey) in (%s)`, 336 strings.Join(vals, ","), 337 ), 338 args..., 339 ) 340 if err != nil { 341 return nil, err 342 } 343 defer rows.Close() 344 var refLinks []models.RichReferenceLink 345 for rows.Next() { 346 var l models.RichReferenceLink 347 l.Kind = models.RefKindIssue 348 if err := rows.Scan(&l.Handle, &l.Repo, &l.SubjectId, &l.Title, &l.State); err != nil { 349 return nil, err 350 } 351 refLinks = append(refLinks, l) 352 } 353 if err := rows.Err(); err != nil { 354 return nil, fmt.Errorf("iterate rows: %w", err) 355 } 356 return refLinks, nil 357} 358 359func getIssueCommentBacklinks(e Execer, aturis []syntax.ATURI) ([]models.RichReferenceLink, error) { 360 if len(aturis) == 0 { 361 return nil, nil 362 } 363 filter := orm.FilterIn("c.at_uri", aturis) 364 rows, err := e.Query( 365 fmt.Sprintf( 366 `select distinct r.did, r.name, i.issue_id, c.id, i.title, i.open 367 from issue_comments c 368 join issues i 369 on i.at_uri = c.issue_at 370 join repos r 371 on coalesce( 372 nullif(i.repo_did, '') = nullif(r.repo_did, ''), 373 i.repo_at = r.at_uri 374 ) 375 where %s`, 376 filter.Condition(), 377 ), 378 filter.Arg()..., 379 ) 380 if err != nil { 381 return nil, err 382 } 383 defer rows.Close() 384 var refLinks []models.RichReferenceLink 385 for rows.Next() { 386 var l models.RichReferenceLink 387 l.Kind = models.RefKindIssue 388 l.CommentId = new(int) 389 if err := rows.Scan(&l.Handle, &l.Repo, &l.SubjectId, l.CommentId, &l.Title, &l.State); err != nil { 390 return nil, err 391 } 392 refLinks = append(refLinks, l) 393 } 394 if err := rows.Err(); err != nil { 395 return nil, fmt.Errorf("iterate rows: %w", err) 396 } 397 return refLinks, nil 398} 399 400func getPullBacklinks(e Execer, aturis []syntax.ATURI) ([]models.RichReferenceLink, error) { 401 if len(aturis) == 0 { 402 return nil, nil 403 } 404 vals := make([]string, len(aturis)) 405 args := make([]any, 0, len(aturis)*2) 406 for i, aturi := range aturis { 407 vals[i] = "(?, ?)" 408 did := aturi.Authority().String() 409 rkey := aturi.RecordKey().String() 410 args = append(args, did, rkey) 411 } 412 rows, err := e.Query( 413 fmt.Sprintf( 414 `select distinct r.did, r.name, p.pull_id, p.title, p.state 415 from pulls p 416 join repos r 417 on coalesce( 418 nullif(p.repo_did, '') = nullif(r.repo_did, ''), 419 p.repo_at = r.at_uri 420 ) 421 where (p.owner_did, p.rkey) in (%s)`, 422 strings.Join(vals, ","), 423 ), 424 args..., 425 ) 426 if err != nil { 427 return nil, err 428 } 429 defer rows.Close() 430 var refLinks []models.RichReferenceLink 431 for rows.Next() { 432 var l models.RichReferenceLink 433 l.Kind = models.RefKindPull 434 if err := rows.Scan(&l.Handle, &l.Repo, &l.SubjectId, &l.Title, &l.State); err != nil { 435 return nil, err 436 } 437 refLinks = append(refLinks, l) 438 } 439 if err := rows.Err(); err != nil { 440 return nil, fmt.Errorf("iterate rows: %w", err) 441 } 442 return refLinks, nil 443} 444 445func getPullCommentBacklinks(e Execer, aturis []syntax.ATURI) ([]models.RichReferenceLink, error) { 446 if len(aturis) == 0 { 447 return nil, nil 448 } 449 filter := orm.FilterIn("c.comment_at", aturis) 450 rows, err := e.Query( 451 fmt.Sprintf( 452 `select distinct r.did, r.name, p.pull_id, c.id, p.title, p.state 453 from repos r 454 join pulls p 455 on coalesce( 456 nullif(p.repo_did, '') = nullif(r.repo_did, ''), 457 p.repo_at = r.at_uri 458 ) 459 join pull_comments c 460 on coalesce( 461 nullif(c.repo_did, '') = nullif(r.repo_did, ''), 462 c.repo_at = r.at_uri 463 ) and p.pull_id = c.pull_id 464 where %s`, 465 filter.Condition(), 466 ), 467 filter.Arg()..., 468 ) 469 if err != nil { 470 return nil, err 471 } 472 defer rows.Close() 473 var refLinks []models.RichReferenceLink 474 for rows.Next() { 475 var l models.RichReferenceLink 476 l.Kind = models.RefKindPull 477 l.CommentId = new(int) 478 if err := rows.Scan(&l.Handle, &l.Repo, &l.SubjectId, l.CommentId, &l.Title, &l.State); err != nil { 479 return nil, err 480 } 481 refLinks = append(refLinks, l) 482 } 483 if err := rows.Err(); err != nil { 484 return nil, fmt.Errorf("iterate rows: %w", err) 485 } 486 return refLinks, nil 487}