This repository has no description
0

Configure Feed

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

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