this repo has no description
1package db 2 3import ( 4 "database/sql" 5 "fmt" 6 "maps" 7 "slices" 8 "sort" 9 "strings" 10 "time" 11 12 "github.com/bluesky-social/indigo/atproto/syntax" 13 "tangled.org/core/appview/models" 14 "tangled.org/core/appview/pagination" 15) 16 17func PutIssue(tx *sql.Tx, issue *models.Issue) error { 18 // ensure sequence exists 19 _, err := tx.Exec(` 20 insert or ignore into repo_issue_seqs (repo_at, next_issue_id) 21 values (?, 1) 22 `, issue.RepoAt) 23 if err != nil { 24 return err 25 } 26 27 issues, err := GetIssues( 28 tx, 29 FilterEq("did", issue.Did), 30 FilterEq("rkey", issue.Rkey), 31 ) 32 switch { 33 case err != nil: 34 return err 35 case len(issues) == 0: 36 return createNewIssue(tx, issue) 37 case len(issues) != 1: // should be unreachable 38 return fmt.Errorf("invalid number of issues returned: %d", len(issues)) 39 default: 40 // if content is identical, do not edit 41 existingIssue := issues[0] 42 if existingIssue.Title == issue.Title && existingIssue.Body == issue.Body { 43 return nil 44 } 45 46 issue.Id = existingIssue.Id 47 issue.IssueId = existingIssue.IssueId 48 return updateIssue(tx, issue) 49 } 50} 51 52func createNewIssue(tx *sql.Tx, issue *models.Issue) error { 53 // get next issue_id 54 var newIssueId int 55 err := tx.QueryRow(` 56 update repo_issue_seqs 57 set next_issue_id = next_issue_id + 1 58 where repo_at = ? 59 returning next_issue_id - 1 60 `, issue.RepoAt).Scan(&newIssueId) 61 if err != nil { 62 return err 63 } 64 65 // insert new issue 66 row := tx.QueryRow(` 67 insert into issues (repo_at, did, rkey, issue_id, title, body) 68 values (?, ?, ?, ?, ?, ?) 69 returning rowid, issue_id 70 `, issue.RepoAt, issue.Did, issue.Rkey, newIssueId, issue.Title, issue.Body) 71 72 return row.Scan(&issue.Id, &issue.IssueId) 73} 74 75func updateIssue(tx *sql.Tx, issue *models.Issue) error { 76 // update existing issue 77 _, err := tx.Exec(` 78 update issues 79 set title = ?, body = ?, edited = ? 80 where did = ? and rkey = ? 81 `, issue.Title, issue.Body, time.Now().Format(time.RFC3339), issue.Did, issue.Rkey) 82 return err 83} 84 85func GetIssuesPaginated(e Execer, page pagination.Page, filters ...filter) ([]models.Issue, error) { 86 issueMap := make(map[string]*models.Issue) // at-uri -> issue 87 88 var conditions []string 89 var args []any 90 91 for _, filter := range filters { 92 conditions = append(conditions, filter.Condition()) 93 args = append(args, filter.Arg()...) 94 } 95 96 whereClause := "" 97 if conditions != nil { 98 whereClause = " where " + strings.Join(conditions, " and ") 99 } 100 101 pLower := FilterGte("row_num", page.Offset+1) 102 pUpper := FilterLte("row_num", page.Offset+page.Limit) 103 104 pageClause := "" 105 if page.Limit > 0 { 106 args = append(args, pLower.Arg()...) 107 args = append(args, pUpper.Arg()...) 108 pageClause = " where " + pLower.Condition() + " and " + pUpper.Condition() 109 } 110 111 query := fmt.Sprintf( 112 ` 113 select * from ( 114 select 115 id, 116 did, 117 rkey, 118 repo_at, 119 issue_id, 120 title, 121 body, 122 open, 123 created, 124 edited, 125 deleted, 126 row_number() over (order by created desc) as row_num 127 from 128 issues 129 %s 130 ) ranked_issues 131 %s 132 `, 133 whereClause, 134 pageClause, 135 ) 136 137 rows, err := e.Query(query, args...) 138 if err != nil { 139 return nil, fmt.Errorf("failed to query issues table: %w", err) 140 } 141 defer rows.Close() 142 143 for rows.Next() { 144 var issue models.Issue 145 var createdAt string 146 var editedAt, deletedAt sql.Null[string] 147 var rowNum int64 148 err := rows.Scan( 149 &issue.Id, 150 &issue.Did, 151 &issue.Rkey, 152 &issue.RepoAt, 153 &issue.IssueId, 154 &issue.Title, 155 &issue.Body, 156 &issue.Open, 157 &createdAt, 158 &editedAt, 159 &deletedAt, 160 &rowNum, 161 ) 162 if err != nil { 163 return nil, fmt.Errorf("failed to scan issue: %w", err) 164 } 165 166 if t, err := time.Parse(time.RFC3339, createdAt); err == nil { 167 issue.Created = t 168 } 169 170 if editedAt.Valid { 171 if t, err := time.Parse(time.RFC3339, editedAt.V); err == nil { 172 issue.Edited = &t 173 } 174 } 175 176 if deletedAt.Valid { 177 if t, err := time.Parse(time.RFC3339, deletedAt.V); err == nil { 178 issue.Deleted = &t 179 } 180 } 181 182 atUri := issue.AtUri().String() 183 issueMap[atUri] = &issue 184 } 185 186 // collect reverse repos 187 repoAts := make([]string, 0, len(issueMap)) // or just []string{} 188 for _, issue := range issueMap { 189 repoAts = append(repoAts, string(issue.RepoAt)) 190 } 191 192 repos, err := GetRepos(e, 0, FilterIn("at_uri", repoAts)) 193 if err != nil { 194 return nil, fmt.Errorf("failed to build repo mappings: %w", err) 195 } 196 197 repoMap := make(map[string]*models.Repo) 198 for i := range repos { 199 repoMap[string(repos[i].RepoAt())] = &repos[i] 200 } 201 202 for issueAt, i := range issueMap { 203 if r, ok := repoMap[string(i.RepoAt)]; ok { 204 i.Repo = r 205 } else { 206 // do not show up the issue if the repo is deleted 207 // TODO: foreign key where? 208 delete(issueMap, issueAt) 209 } 210 } 211 212 // collect comments 213 issueAts := slices.Collect(maps.Keys(issueMap)) 214 215 comments, err := GetIssueComments(e, FilterIn("issue_at", issueAts)) 216 if err != nil { 217 return nil, fmt.Errorf("failed to query comments: %w", err) 218 } 219 for i := range comments { 220 issueAt := comments[i].IssueAt 221 if issue, ok := issueMap[issueAt]; ok { 222 issue.Comments = append(issue.Comments, comments[i]) 223 } 224 } 225 226 // collect allLabels for each issue 227 allLabels, err := GetLabels(e, FilterIn("subject", issueAts)) 228 if err != nil { 229 return nil, fmt.Errorf("failed to query labels: %w", err) 230 } 231 for issueAt, labels := range allLabels { 232 if issue, ok := issueMap[issueAt.String()]; ok { 233 issue.Labels = labels 234 } 235 } 236 237 var issues []models.Issue 238 for _, i := range issueMap { 239 issues = append(issues, *i) 240 } 241 242 sort.Slice(issues, func(i, j int) bool { 243 return issues[i].Created.After(issues[j].Created) 244 }) 245 246 return issues, nil 247} 248 249func GetIssues(e Execer, filters ...filter) ([]models.Issue, error) { 250 return GetIssuesPaginated(e, pagination.Page{}, filters...) 251} 252 253func AddIssueComment(e Execer, c models.IssueComment) (int64, error) { 254 result, err := e.Exec( 255 `insert into issue_comments ( 256 did, 257 rkey, 258 issue_at, 259 body, 260 reply_to, 261 created, 262 edited 263 ) 264 values (?, ?, ?, ?, ?, ?, null) 265 on conflict(did, rkey) do update set 266 issue_at = excluded.issue_at, 267 body = excluded.body, 268 edited = case 269 when 270 issue_comments.issue_at != excluded.issue_at 271 or issue_comments.body != excluded.body 272 or issue_comments.reply_to != excluded.reply_to 273 then ? 274 else issue_comments.edited 275 end`, 276 c.Did, 277 c.Rkey, 278 c.IssueAt, 279 c.Body, 280 c.ReplyTo, 281 c.Created.Format(time.RFC3339), 282 time.Now().Format(time.RFC3339), 283 ) 284 if err != nil { 285 return 0, err 286 } 287 288 id, err := result.LastInsertId() 289 if err != nil { 290 return 0, err 291 } 292 293 return id, nil 294} 295 296func DeleteIssueComments(e Execer, filters ...filter) error { 297 var conditions []string 298 var args []any 299 for _, filter := range filters { 300 conditions = append(conditions, filter.Condition()) 301 args = append(args, filter.Arg()...) 302 } 303 304 whereClause := "" 305 if conditions != nil { 306 whereClause = " where " + strings.Join(conditions, " and ") 307 } 308 309 query := fmt.Sprintf(`update issue_comments set body = "", deleted = strftime('%%Y-%%m-%%dT%%H:%%M:%%SZ', 'now') %s`, whereClause) 310 311 _, err := e.Exec(query, args...) 312 return err 313} 314 315func GetIssueComments(e Execer, filters ...filter) ([]models.IssueComment, error) { 316 var comments []models.IssueComment 317 318 var conditions []string 319 var args []any 320 for _, filter := range filters { 321 conditions = append(conditions, filter.Condition()) 322 args = append(args, filter.Arg()...) 323 } 324 325 whereClause := "" 326 if conditions != nil { 327 whereClause = " where " + strings.Join(conditions, " and ") 328 } 329 330 query := fmt.Sprintf(` 331 select 332 id, 333 did, 334 rkey, 335 issue_at, 336 reply_to, 337 body, 338 created, 339 edited, 340 deleted 341 from 342 issue_comments 343 %s 344 `, whereClause) 345 346 rows, err := e.Query(query, args...) 347 if err != nil { 348 return nil, err 349 } 350 351 for rows.Next() { 352 var comment models.IssueComment 353 var created string 354 var rkey, edited, deleted, replyTo sql.Null[string] 355 err := rows.Scan( 356 &comment.Id, 357 &comment.Did, 358 &rkey, 359 &comment.IssueAt, 360 &replyTo, 361 &comment.Body, 362 &created, 363 &edited, 364 &deleted, 365 ) 366 if err != nil { 367 return nil, err 368 } 369 370 // this is a remnant from old times, newer comments always have rkey 371 if rkey.Valid { 372 comment.Rkey = rkey.V 373 } 374 375 if t, err := time.Parse(time.RFC3339, created); err == nil { 376 comment.Created = t 377 } 378 379 if edited.Valid { 380 if t, err := time.Parse(time.RFC3339, edited.V); err == nil { 381 comment.Edited = &t 382 } 383 } 384 385 if deleted.Valid { 386 if t, err := time.Parse(time.RFC3339, deleted.V); err == nil { 387 comment.Deleted = &t 388 } 389 } 390 391 if replyTo.Valid { 392 comment.ReplyTo = &replyTo.V 393 } 394 395 comments = append(comments, comment) 396 } 397 398 if err = rows.Err(); err != nil { 399 return nil, err 400 } 401 402 return comments, nil 403} 404 405func DeleteIssues(e Execer, filters ...filter) error { 406 var conditions []string 407 var args []any 408 for _, filter := range filters { 409 conditions = append(conditions, filter.Condition()) 410 args = append(args, filter.Arg()...) 411 } 412 413 whereClause := "" 414 if conditions != nil { 415 whereClause = " where " + strings.Join(conditions, " and ") 416 } 417 418 query := fmt.Sprintf(`delete from issues %s`, whereClause) 419 _, err := e.Exec(query, args...) 420 return err 421} 422 423func CloseIssues(e Execer, filters ...filter) error { 424 var conditions []string 425 var args []any 426 for _, filter := range filters { 427 conditions = append(conditions, filter.Condition()) 428 args = append(args, filter.Arg()...) 429 } 430 431 whereClause := "" 432 if conditions != nil { 433 whereClause = " where " + strings.Join(conditions, " and ") 434 } 435 436 query := fmt.Sprintf(`update issues set open = 0 %s`, whereClause) 437 _, err := e.Exec(query, args...) 438 return err 439} 440 441func ReopenIssues(e Execer, filters ...filter) error { 442 var conditions []string 443 var args []any 444 for _, filter := range filters { 445 conditions = append(conditions, filter.Condition()) 446 args = append(args, filter.Arg()...) 447 } 448 449 whereClause := "" 450 if conditions != nil { 451 whereClause = " where " + strings.Join(conditions, " and ") 452 } 453 454 query := fmt.Sprintf(`update issues set open = 1 %s`, whereClause) 455 _, err := e.Exec(query, args...) 456 return err 457} 458 459func GetIssueCount(e Execer, repoAt syntax.ATURI) (models.IssueCount, error) { 460 row := e.QueryRow(` 461 select 462 count(case when open = 1 then 1 end) as open_count, 463 count(case when open = 0 then 1 end) as closed_count 464 from issues 465 where repo_at = ?`, 466 repoAt, 467 ) 468 469 var count models.IssueCount 470 if err := row.Scan(&count.Open, &count.Closed); err != nil { 471 return models.IssueCount{}, err 472 } 473 474 return count, nil 475}