Monorepo for Tangled tangled.org
at sl/shared-stacks 454 lines 11 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/Comment 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.at_uri, c.at_uri 57 from input inp 58 join repos r 59 on r.did = inp.owner_did 60 and r.name = inp.name 61 join issues i 62 on i.repo_at = r.at_uri 63 and i.issue_id = inp.issue_id 64 left join comments c 65 on inp.comment_id is not null 66 and c.subject_at = i.at_uri 67 and c.id = inp.comment_id 68 `, 69 strings.Join(vals, ","), 70 ) 71 rows, err := e.Query(query, args...) 72 if err != nil { 73 return nil, err 74 } 75 defer rows.Close() 76 77 var uris []syntax.ATURI 78 79 for rows.Next() { 80 // Scan rows 81 var issueUri string 82 var commentUri sql.NullString 83 var uri syntax.ATURI 84 if err := rows.Scan(&issueUri, &commentUri); err != nil { 85 return nil, err 86 } 87 if commentUri.Valid { 88 uri = syntax.ATURI(commentUri.String) 89 } else { 90 uri = syntax.ATURI(issueUri) 91 } 92 uris = append(uris, uri) 93 } 94 if err := rows.Err(); err != nil { 95 return nil, fmt.Errorf("iterate rows: %w", err) 96 } 97 98 return uris, nil 99} 100 101func findPullReferences(e Execer, refLinks []models.ReferenceLink) ([]syntax.ATURI, error) { 102 if len(refLinks) == 0 { 103 return nil, nil 104 } 105 vals := make([]string, len(refLinks)) 106 args := make([]any, 0, len(refLinks)*4) 107 for i, ref := range refLinks { 108 vals[i] = "(?, ?, ?, ?)" 109 args = append(args, ref.Handle, ref.Repo, ref.SubjectId, ref.CommentId) 110 } 111 query := fmt.Sprintf( 112 `with input(owner_did, name, pull_id, comment_id) as ( 113 values %s 114 ) 115 select 116 p.owner_did, p.rkey, c.at_uri 117 from input inp 118 join repos r 119 on r.did = inp.owner_did 120 and r.name = inp.name 121 join pulls p 122 on p.repo_at = r.at_uri 123 and p.pull_id = inp.pull_id 124 left join comments c 125 on inp.comment_id is not null 126 and c.subject_at = ('at://' || p.owner_did || '/' || 'sh.tangled.repo.pull' || '/' || p.rkey) 127 and c.id = inp.comment_id 128 `, 129 strings.Join(vals, ","), 130 ) 131 rows, err := e.Query(query, args...) 132 if err != nil { 133 return nil, err 134 } 135 defer rows.Close() 136 137 var uris []syntax.ATURI 138 139 for rows.Next() { 140 // Scan rows 141 var pullOwner, pullRkey string 142 var commentUri sql.NullString 143 var uri syntax.ATURI 144 if err := rows.Scan(&pullOwner, &pullRkey, &commentUri); err != nil { 145 return nil, err 146 } 147 if commentUri.Valid { 148 // no-op 149 uri = syntax.ATURI(commentUri.String) 150 } else { 151 uri = syntax.ATURI(fmt.Sprintf( 152 "at://%s/%s/%s", 153 pullOwner, 154 tangled.RepoPullNSID, 155 pullRkey, 156 )) 157 } 158 uris = append(uris, uri) 159 } 160 return uris, nil 161} 162 163func putReferences(tx *sql.Tx, fromAt syntax.ATURI, fromCid *syntax.CID, references []syntax.ATURI) error { 164 // clear existing referneces when cid isn't provided 165 if fromCid == nil { 166 err := deleteReferences(tx, fromAt) 167 if err != nil { 168 return fmt.Errorf("delete old reference_links: %w", err) 169 } 170 if len(references) == 0 { 171 return nil 172 } 173 } 174 175 values := make([]string, 0, len(references)) 176 args := make([]any, 0, len(references)*2) 177 for _, ref := range references { 178 values = append(values, "(?, ?, ?)") 179 args = append(args, fromAt, fromCid, ref) 180 } 181 _, err := tx.Exec( 182 fmt.Sprintf( 183 `insert into reference_links (from_at, from_cid, to_at) 184 values %s`, 185 strings.Join(values, ","), 186 ), 187 args..., 188 ) 189 if err != nil { 190 return fmt.Errorf("insert new reference_links: %w", err) 191 } 192 return nil 193} 194 195func deleteReferences(tx *sql.Tx, fromAt syntax.ATURI) error { 196 _, err := tx.Exec(`delete from reference_links where from_at = ?`, fromAt) 197 return err 198} 199 200func GetReferencesAll(e Execer, filters ...orm.Filter) (map[syntax.ATURI][]syntax.ATURI, error) { 201 var ( 202 conditions []string 203 args []any 204 ) 205 for _, filter := range filters { 206 conditions = append(conditions, filter.Condition()) 207 args = append(args, filter.Arg()...) 208 } 209 210 whereClause := "" 211 if conditions != nil { 212 whereClause = " where " + strings.Join(conditions, " and ") 213 } 214 215 rows, err := e.Query( 216 fmt.Sprintf( 217 `select from_at, to_at from reference_links %s`, 218 whereClause, 219 ), 220 args..., 221 ) 222 if err != nil { 223 return nil, fmt.Errorf("query reference_links: %w", err) 224 } 225 defer rows.Close() 226 227 result := make(map[syntax.ATURI][]syntax.ATURI) 228 229 for rows.Next() { 230 var from, to syntax.ATURI 231 if err := rows.Scan(&from, &to); err != nil { 232 return nil, fmt.Errorf("scan row: %w", err) 233 } 234 235 result[from] = append(result[from], to) 236 } 237 if err := rows.Err(); err != nil { 238 return nil, fmt.Errorf("iterate rows: %w", err) 239 } 240 241 return result, nil 242} 243 244func GetBacklinks(e Execer, target syntax.ATURI) ([]models.RichReferenceLink, error) { 245 rows, err := e.Query( 246 `select from_at from reference_links 247 where to_at = ?`, 248 target, 249 ) 250 if err != nil { 251 return nil, fmt.Errorf("query backlinks: %w", err) 252 } 253 defer rows.Close() 254 255 var ( 256 backlinks []models.RichReferenceLink 257 backlinksMap = make(map[string][]syntax.ATURI) 258 ) 259 for rows.Next() { 260 var from syntax.ATURI 261 if err := rows.Scan(&from); err != nil { 262 return nil, fmt.Errorf("scan row: %w", err) 263 } 264 nsid := from.Collection().String() 265 backlinksMap[nsid] = append(backlinksMap[nsid], from) 266 } 267 if err := rows.Err(); err != nil { 268 return nil, fmt.Errorf("iterate rows: %w", err) 269 } 270 271 var ls []models.RichReferenceLink 272 ls, err = getIssueBacklinks(e, backlinksMap[tangled.RepoIssueNSID]) 273 if err != nil { 274 return nil, fmt.Errorf("get issue backlinks: %w", err) 275 } 276 backlinks = append(backlinks, ls...) 277 ls, err = getIssueCommentBacklinks(e, backlinksMap[tangled.CommentNSID]) 278 if err != nil { 279 return nil, fmt.Errorf("get issue_comment backlinks: %w", err) 280 } 281 backlinks = append(backlinks, ls...) 282 ls, err = getPullBacklinks(e, backlinksMap[tangled.RepoPullNSID]) 283 if err != nil { 284 return nil, fmt.Errorf("get pull backlinks: %w", err) 285 } 286 backlinks = append(backlinks, ls...) 287 ls, err = getPullCommentBacklinks(e, backlinksMap[tangled.CommentNSID]) 288 if err != nil { 289 return nil, fmt.Errorf("get pull_comment backlinks: %w", err) 290 } 291 backlinks = append(backlinks, ls...) 292 293 return backlinks, nil 294} 295 296func getIssueBacklinks(e Execer, aturis []syntax.ATURI) ([]models.RichReferenceLink, error) { 297 if len(aturis) == 0 { 298 return nil, nil 299 } 300 vals := make([]string, len(aturis)) 301 args := make([]any, 0, len(aturis)*2) 302 for i, aturi := range aturis { 303 vals[i] = "(?, ?)" 304 did := aturi.Authority().String() 305 rkey := aturi.RecordKey().String() 306 args = append(args, did, rkey) 307 } 308 rows, err := e.Query( 309 fmt.Sprintf( 310 `select r.did, r.name, i.issue_id, i.title, i.open 311 from issues i 312 join repos r 313 on r.at_uri = i.repo_at 314 where (i.did, i.rkey) in (%s)`, 315 strings.Join(vals, ","), 316 ), 317 args..., 318 ) 319 if err != nil { 320 return nil, err 321 } 322 defer rows.Close() 323 var refLinks []models.RichReferenceLink 324 for rows.Next() { 325 var l models.RichReferenceLink 326 l.Kind = models.RefKindIssue 327 if err := rows.Scan(&l.Handle, &l.Repo, &l.SubjectId, &l.Title, &l.State); err != nil { 328 return nil, err 329 } 330 refLinks = append(refLinks, l) 331 } 332 if err := rows.Err(); err != nil { 333 return nil, fmt.Errorf("iterate rows: %w", err) 334 } 335 return refLinks, nil 336} 337 338func getIssueCommentBacklinks(e Execer, aturis []syntax.ATURI) ([]models.RichReferenceLink, error) { 339 if len(aturis) == 0 { 340 return nil, nil 341 } 342 filter := orm.FilterIn("c.at_uri", aturis) 343 rows, err := e.Query( 344 fmt.Sprintf( 345 `select r.did, r.name, i.issue_id, c.id, i.title, i.open 346 from comments c 347 join issues i 348 on i.at_uri = c.subject_at 349 join repos r 350 on r.at_uri = i.repo_at 351 where %s`, 352 filter.Condition(), 353 ), 354 filter.Arg()..., 355 ) 356 if err != nil { 357 return nil, err 358 } 359 defer rows.Close() 360 var refLinks []models.RichReferenceLink 361 for rows.Next() { 362 var l models.RichReferenceLink 363 l.Kind = models.RefKindIssue 364 l.CommentId = new(int) 365 if err := rows.Scan(&l.Handle, &l.Repo, &l.SubjectId, l.CommentId, &l.Title, &l.State); err != nil { 366 return nil, err 367 } 368 refLinks = append(refLinks, l) 369 } 370 if err := rows.Err(); err != nil { 371 return nil, fmt.Errorf("iterate rows: %w", err) 372 } 373 return refLinks, nil 374} 375 376func getPullBacklinks(e Execer, aturis []syntax.ATURI) ([]models.RichReferenceLink, error) { 377 if len(aturis) == 0 { 378 return nil, nil 379 } 380 vals := make([]string, len(aturis)) 381 args := make([]any, 0, len(aturis)*2) 382 for i, aturi := range aturis { 383 vals[i] = "(?, ?)" 384 did := aturi.Authority().String() 385 rkey := aturi.RecordKey().String() 386 args = append(args, did, rkey) 387 } 388 rows, err := e.Query( 389 fmt.Sprintf( 390 `select r.did, r.name, p.pull_id, p.title, p.state 391 from pulls p 392 join repos r 393 on r.at_uri = p.repo_at 394 where (p.owner_did, p.rkey) in (%s)`, 395 strings.Join(vals, ","), 396 ), 397 args..., 398 ) 399 if err != nil { 400 return nil, err 401 } 402 defer rows.Close() 403 var refLinks []models.RichReferenceLink 404 for rows.Next() { 405 var l models.RichReferenceLink 406 l.Kind = models.RefKindPull 407 if err := rows.Scan(&l.Handle, &l.Repo, &l.SubjectId, &l.Title, &l.State); err != nil { 408 return nil, err 409 } 410 refLinks = append(refLinks, l) 411 } 412 if err := rows.Err(); err != nil { 413 return nil, fmt.Errorf("iterate rows: %w", err) 414 } 415 return refLinks, nil 416} 417 418func getPullCommentBacklinks(e Execer, aturis []syntax.ATURI) ([]models.RichReferenceLink, error) { 419 if len(aturis) == 0 { 420 return nil, nil 421 } 422 filter := orm.FilterIn("c.at_uri", aturis) 423 rows, err := e.Query( 424 fmt.Sprintf( 425 `select r.did, r.name, p.pull_id, c.id, p.title, p.state 426 from repos r 427 join pulls p 428 on r.at_uri = p.repo_at 429 join comments c 430 on ('at://' || p.owner_did || '/' || 'sh.tangled.repo.pull' || '/' || p.rkey) = c.subject_at 431 where %s`, 432 filter.Condition(), 433 ), 434 filter.Arg()..., 435 ) 436 if err != nil { 437 return nil, err 438 } 439 defer rows.Close() 440 var refLinks []models.RichReferenceLink 441 for rows.Next() { 442 var l models.RichReferenceLink 443 l.Kind = models.RefKindPull 444 l.CommentId = new(int) 445 if err := rows.Scan(&l.Handle, &l.Repo, &l.SubjectId, l.CommentId, &l.Title, &l.State); err != nil { 446 return nil, err 447 } 448 refLinks = append(refLinks, l) 449 } 450 if err := rows.Err(); err != nil { 451 return nil, fmt.Errorf("iterate rows: %w", err) 452 } 453 return refLinks, nil 454}