this repo has no description
1package db 2 3import ( 4 "database/sql" 5 "time" 6 7 "github.com/bluesky-social/indigo/atproto/syntax" 8 "tangled.sh/tangled.sh/core/appview/pagination" 9) 10 11type Issue struct { 12 ID int64 13 RepoAt syntax.ATURI 14 OwnerDid string 15 IssueId int 16 IssueAt string 17 Created time.Time 18 Title string 19 Body string 20 Open bool 21 22 // optionally, populate this when querying for reverse mappings 23 // like comment counts, parent repo etc. 24 Metadata *IssueMetadata 25} 26 27type IssueMetadata struct { 28 CommentCount int 29 Repo *Repo 30 // labels, assignee etc. 31} 32 33type Comment struct { 34 OwnerDid string 35 RepoAt syntax.ATURI 36 Rkey string 37 Issue int 38 CommentId int 39 Body string 40 Created *time.Time 41 Deleted *time.Time 42 Edited *time.Time 43} 44 45func NewIssue(tx *sql.Tx, issue *Issue) error { 46 defer tx.Rollback() 47 48 _, err := tx.Exec(` 49 insert or ignore into repo_issue_seqs (repo_at, next_issue_id) 50 values (?, 1) 51 `, issue.RepoAt) 52 if err != nil { 53 return err 54 } 55 56 var nextId int 57 err = tx.QueryRow(` 58 update repo_issue_seqs 59 set next_issue_id = next_issue_id + 1 60 where repo_at = ? 61 returning next_issue_id - 1 62 `, issue.RepoAt).Scan(&nextId) 63 if err != nil { 64 return err 65 } 66 67 issue.IssueId = nextId 68 69 res, err := tx.Exec(` 70 insert into issues (repo_at, owner_did, issue_id, title, body) 71 values (?, ?, ?, ?, ?) 72 `, issue.RepoAt, issue.OwnerDid, issue.IssueId, issue.Title, issue.Body) 73 if err != nil { 74 return err 75 } 76 77 lastID, err := res.LastInsertId() 78 if err != nil { 79 return err 80 } 81 issue.ID = lastID 82 83 if err := tx.Commit(); err != nil { 84 return err 85 } 86 87 return nil 88} 89 90func SetIssueAt(e Execer, repoAt syntax.ATURI, issueId int, issueAt string) error { 91 _, err := e.Exec(`update issues set issue_at = ? where repo_at = ? and issue_id = ?`, issueAt, repoAt, issueId) 92 return err 93} 94 95func GetIssueAt(e Execer, repoAt syntax.ATURI, issueId int) (string, error) { 96 var issueAt string 97 err := e.QueryRow(`select issue_at from issues where repo_at = ? and issue_id = ?`, repoAt, issueId).Scan(&issueAt) 98 return issueAt, err 99} 100 101func GetIssueId(e Execer, repoAt syntax.ATURI) (int, error) { 102 var issueId int 103 err := e.QueryRow(`select next_issue_id from repo_issue_seqs where repo_at = ?`, repoAt).Scan(&issueId) 104 return issueId - 1, err 105} 106 107func GetIssueOwnerDid(e Execer, repoAt syntax.ATURI, issueId int) (string, error) { 108 var ownerDid string 109 err := e.QueryRow(`select owner_did from issues where repo_at = ? and issue_id = ?`, repoAt, issueId).Scan(&ownerDid) 110 return ownerDid, err 111} 112 113func GetIssues(e Execer, repoAt syntax.ATURI, isOpen bool, page pagination.Page) ([]Issue, error) { 114 var issues []Issue 115 openValue := 0 116 if isOpen { 117 openValue = 1 118 } 119 120 rows, err := e.Query( 121 ` 122 with numbered_issue as ( 123 select 124 i.id, 125 i.owner_did, 126 i.issue_id, 127 i.created, 128 i.title, 129 i.body, 130 i.open, 131 count(c.id) as comment_count, 132 row_number() over (order by i.created desc) as row_num 133 from 134 issues i 135 left join 136 comments c on i.repo_at = c.repo_at and i.issue_id = c.issue_id 137 where 138 i.repo_at = ? and i.open = ? 139 group by 140 i.id, i.owner_did, i.issue_id, i.created, i.title, i.body, i.open 141 ) 142 select 143 id, 144 owner_did, 145 issue_id, 146 created, 147 title, 148 body, 149 open, 150 comment_count 151 from 152 numbered_issue 153 where 154 row_num between ? and ?`, 155 repoAt, openValue, page.Offset+1, page.Offset+page.Limit) 156 if err != nil { 157 return nil, err 158 } 159 defer rows.Close() 160 161 for rows.Next() { 162 var issue Issue 163 var createdAt string 164 var metadata IssueMetadata 165 err := rows.Scan(&issue.ID, &issue.OwnerDid, &issue.IssueId, &createdAt, &issue.Title, &issue.Body, &issue.Open, &metadata.CommentCount) 166 if err != nil { 167 return nil, err 168 } 169 170 createdTime, err := time.Parse(time.RFC3339, createdAt) 171 if err != nil { 172 return nil, err 173 } 174 issue.Created = createdTime 175 issue.Metadata = &metadata 176 177 issues = append(issues, issue) 178 } 179 180 if err := rows.Err(); err != nil { 181 return nil, err 182 } 183 184 return issues, nil 185} 186 187// timeframe here is directly passed into the sql query filter, and any 188// timeframe in the past should be negative; e.g.: "-3 months" 189func GetIssuesByOwnerDid(e Execer, ownerDid string, timeframe string) ([]Issue, error) { 190 var issues []Issue 191 192 rows, err := e.Query( 193 `select 194 i.id, 195 i.owner_did, 196 i.repo_at, 197 i.issue_id, 198 i.created, 199 i.title, 200 i.body, 201 i.open, 202 r.did, 203 r.name, 204 r.knot, 205 r.rkey, 206 r.created 207 from 208 issues i 209 join 210 repos r on i.repo_at = r.at_uri 211 where 212 i.owner_did = ? and i.created >= date ('now', ?) 213 order by 214 i.created desc`, 215 ownerDid, timeframe) 216 if err != nil { 217 return nil, err 218 } 219 defer rows.Close() 220 221 for rows.Next() { 222 var issue Issue 223 var issueCreatedAt, repoCreatedAt string 224 var repo Repo 225 err := rows.Scan( 226 &issue.ID, 227 &issue.OwnerDid, 228 &issue.RepoAt, 229 &issue.IssueId, 230 &issueCreatedAt, 231 &issue.Title, 232 &issue.Body, 233 &issue.Open, 234 &repo.Did, 235 &repo.Name, 236 &repo.Knot, 237 &repo.Rkey, 238 &repoCreatedAt, 239 ) 240 if err != nil { 241 return nil, err 242 } 243 244 issueCreatedTime, err := time.Parse(time.RFC3339, issueCreatedAt) 245 if err != nil { 246 return nil, err 247 } 248 issue.Created = issueCreatedTime 249 250 repoCreatedTime, err := time.Parse(time.RFC3339, repoCreatedAt) 251 if err != nil { 252 return nil, err 253 } 254 repo.Created = repoCreatedTime 255 256 issue.Metadata = &IssueMetadata{ 257 Repo: &repo, 258 } 259 260 issues = append(issues, issue) 261 } 262 263 if err := rows.Err(); err != nil { 264 return nil, err 265 } 266 267 return issues, nil 268} 269 270func GetIssue(e Execer, repoAt syntax.ATURI, issueId int) (*Issue, error) { 271 query := `select id, owner_did, created, title, body, open from issues where repo_at = ? and issue_id = ?` 272 row := e.QueryRow(query, repoAt, issueId) 273 274 var issue Issue 275 var createdAt string 276 err := row.Scan(&issue.ID, &issue.OwnerDid, &createdAt, &issue.Title, &issue.Body, &issue.Open) 277 if err != nil { 278 return nil, err 279 } 280 281 createdTime, err := time.Parse(time.RFC3339, createdAt) 282 if err != nil { 283 return nil, err 284 } 285 issue.Created = createdTime 286 287 return &issue, nil 288} 289 290func GetIssueWithComments(e Execer, repoAt syntax.ATURI, issueId int) (*Issue, []Comment, error) { 291 query := `select id, owner_did, issue_id, created, title, body, open, issue_at from issues where repo_at = ? and issue_id = ?` 292 row := e.QueryRow(query, repoAt, issueId) 293 294 var issue Issue 295 var createdAt string 296 err := row.Scan(&issue.ID, &issue.OwnerDid, &issue.IssueId, &createdAt, &issue.Title, &issue.Body, &issue.Open, &issue.IssueAt) 297 if err != nil { 298 return nil, nil, err 299 } 300 301 createdTime, err := time.Parse(time.RFC3339, createdAt) 302 if err != nil { 303 return nil, nil, err 304 } 305 issue.Created = createdTime 306 307 comments, err := GetComments(e, repoAt, issueId) 308 if err != nil { 309 return nil, nil, err 310 } 311 312 return &issue, comments, nil 313} 314 315func NewIssueComment(e Execer, comment *Comment) error { 316 query := `insert into comments (owner_did, repo_at, rkey, issue_id, comment_id, body) values (?, ?, ?, ?, ?, ?)` 317 _, err := e.Exec( 318 query, 319 comment.OwnerDid, 320 comment.RepoAt, 321 comment.Rkey, 322 comment.Issue, 323 comment.CommentId, 324 comment.Body, 325 ) 326 return err 327} 328 329func GetComments(e Execer, repoAt syntax.ATURI, issueId int) ([]Comment, error) { 330 var comments []Comment 331 332 rows, err := e.Query(` 333 select 334 owner_did, 335 issue_id, 336 comment_id, 337 rkey, 338 body, 339 created, 340 edited, 341 deleted 342 from 343 comments 344 where 345 repo_at = ? and issue_id = ? 346 order by 347 created asc`, 348 repoAt, 349 issueId, 350 ) 351 if err == sql.ErrNoRows { 352 return []Comment{}, nil 353 } 354 if err != nil { 355 return nil, err 356 } 357 defer rows.Close() 358 359 for rows.Next() { 360 var comment Comment 361 var createdAt string 362 var deletedAt, editedAt, rkey sql.NullString 363 err := rows.Scan(&comment.OwnerDid, &comment.Issue, &comment.CommentId, &rkey, &comment.Body, &createdAt, &editedAt, &deletedAt) 364 if err != nil { 365 return nil, err 366 } 367 368 createdAtTime, err := time.Parse(time.RFC3339, createdAt) 369 if err != nil { 370 return nil, err 371 } 372 comment.Created = &createdAtTime 373 374 if deletedAt.Valid { 375 deletedTime, err := time.Parse(time.RFC3339, deletedAt.String) 376 if err != nil { 377 return nil, err 378 } 379 comment.Deleted = &deletedTime 380 } 381 382 if editedAt.Valid { 383 editedTime, err := time.Parse(time.RFC3339, editedAt.String) 384 if err != nil { 385 return nil, err 386 } 387 comment.Edited = &editedTime 388 } 389 390 if rkey.Valid { 391 comment.Rkey = rkey.String 392 } 393 394 comments = append(comments, comment) 395 } 396 397 if err := rows.Err(); err != nil { 398 return nil, err 399 } 400 401 return comments, nil 402} 403 404func GetComment(e Execer, repoAt syntax.ATURI, issueId, commentId int) (*Comment, error) { 405 query := ` 406 select 407 owner_did, body, rkey, created, deleted, edited 408 from 409 comments where repo_at = ? and issue_id = ? and comment_id = ? 410 ` 411 row := e.QueryRow(query, repoAt, issueId, commentId) 412 413 var comment Comment 414 var createdAt string 415 var deletedAt, editedAt, rkey sql.NullString 416 err := row.Scan(&comment.OwnerDid, &comment.Body, &rkey, &createdAt, &deletedAt, &editedAt) 417 if err != nil { 418 return nil, err 419 } 420 421 createdTime, err := time.Parse(time.RFC3339, createdAt) 422 if err != nil { 423 return nil, err 424 } 425 comment.Created = &createdTime 426 427 if deletedAt.Valid { 428 deletedTime, err := time.Parse(time.RFC3339, deletedAt.String) 429 if err != nil { 430 return nil, err 431 } 432 comment.Deleted = &deletedTime 433 } 434 435 if editedAt.Valid { 436 editedTime, err := time.Parse(time.RFC3339, editedAt.String) 437 if err != nil { 438 return nil, err 439 } 440 comment.Edited = &editedTime 441 } 442 443 if rkey.Valid { 444 comment.Rkey = rkey.String 445 } 446 447 comment.RepoAt = repoAt 448 comment.Issue = issueId 449 comment.CommentId = commentId 450 451 return &comment, nil 452} 453 454func EditComment(e Execer, repoAt syntax.ATURI, issueId, commentId int, newBody string) error { 455 _, err := e.Exec( 456 ` 457 update comments 458 set body = ?, 459 edited = strftime('%Y-%m-%dT%H:%M:%SZ', 'now') 460 where repo_at = ? and issue_id = ? and comment_id = ? 461 `, newBody, repoAt, issueId, commentId) 462 return err 463} 464 465func DeleteComment(e Execer, repoAt syntax.ATURI, issueId, commentId int) error { 466 _, err := e.Exec( 467 ` 468 update comments 469 set body = "", 470 deleted = strftime('%Y-%m-%dT%H:%M:%SZ', 'now') 471 where repo_at = ? and issue_id = ? and comment_id = ? 472 `, repoAt, issueId, commentId) 473 return err 474} 475 476func CloseIssue(e Execer, repoAt syntax.ATURI, issueId int) error { 477 _, err := e.Exec(`update issues set open = 0 where repo_at = ? and issue_id = ?`, repoAt, issueId) 478 return err 479} 480 481func ReopenIssue(e Execer, repoAt syntax.ATURI, issueId int) error { 482 _, err := e.Exec(`update issues set open = 1 where repo_at = ? and issue_id = ?`, repoAt, issueId) 483 return err 484} 485 486type IssueCount struct { 487 Open int 488 Closed int 489} 490 491func GetIssueCount(e Execer, repoAt syntax.ATURI) (IssueCount, error) { 492 row := e.QueryRow(` 493 select 494 count(case when open = 1 then 1 end) as open_count, 495 count(case when open = 0 then 1 end) as closed_count 496 from issues 497 where repo_at = ?`, 498 repoAt, 499 ) 500 501 var count IssueCount 502 if err := row.Scan(&count.Open, &count.Closed); err != nil { 503 return IssueCount{0, 0}, err 504 } 505 506 return count, nil 507}