forked from tangled.org/core
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 GetIssueOwnerDid(e Execer, repoAt syntax.ATURI, issueId int) (string, error) { 102 var ownerDid string 103 err := e.QueryRow(`select owner_did from issues where repo_at = ? and issue_id = ?`, repoAt, issueId).Scan(&ownerDid) 104 return ownerDid, err 105} 106 107func GetIssues(e Execer, repoAt syntax.ATURI, isOpen bool, page pagination.Page) ([]Issue, error) { 108 var issues []Issue 109 openValue := 0 110 if isOpen { 111 openValue = 1 112 } 113 114 rows, err := e.Query( 115 ` 116 with numbered_issue as ( 117 select 118 i.id, 119 i.owner_did, 120 i.issue_id, 121 i.created, 122 i.title, 123 i.body, 124 i.open, 125 count(c.id) as comment_count, 126 row_number() over (order by i.created desc) as row_num 127 from 128 issues i 129 left join 130 comments c on i.repo_at = c.repo_at and i.issue_id = c.issue_id 131 where 132 i.repo_at = ? and i.open = ? 133 group by 134 i.id, i.owner_did, i.issue_id, i.created, i.title, i.body, i.open 135 ) 136 select 137 id, 138 owner_did, 139 issue_id, 140 created, 141 title, 142 body, 143 open, 144 comment_count 145 from 146 numbered_issue 147 where 148 row_num between ? and ?`, 149 repoAt, openValue, page.Offset+1, page.Offset+page.Limit) 150 if err != nil { 151 return nil, err 152 } 153 defer rows.Close() 154 155 for rows.Next() { 156 var issue Issue 157 var createdAt string 158 var metadata IssueMetadata 159 err := rows.Scan(&issue.ID, &issue.OwnerDid, &issue.IssueId, &createdAt, &issue.Title, &issue.Body, &issue.Open, &metadata.CommentCount) 160 if err != nil { 161 return nil, err 162 } 163 164 createdTime, err := time.Parse(time.RFC3339, createdAt) 165 if err != nil { 166 return nil, err 167 } 168 issue.Created = createdTime 169 issue.Metadata = &metadata 170 171 issues = append(issues, issue) 172 } 173 174 if err := rows.Err(); err != nil { 175 return nil, err 176 } 177 178 return issues, nil 179} 180 181// timeframe here is directly passed into the sql query filter, and any 182// timeframe in the past should be negative; e.g.: "-3 months" 183func GetIssuesByOwnerDid(e Execer, ownerDid string, timeframe string) ([]Issue, error) { 184 var issues []Issue 185 186 rows, err := e.Query( 187 `select 188 i.id, 189 i.owner_did, 190 i.repo_at, 191 i.issue_id, 192 i.created, 193 i.title, 194 i.body, 195 i.open, 196 r.did, 197 r.name, 198 r.knot, 199 r.rkey, 200 r.created 201 from 202 issues i 203 join 204 repos r on i.repo_at = r.at_uri 205 where 206 i.owner_did = ? and i.created >= date ('now', ?) 207 order by 208 i.created desc`, 209 ownerDid, timeframe) 210 if err != nil { 211 return nil, err 212 } 213 defer rows.Close() 214 215 for rows.Next() { 216 var issue Issue 217 var issueCreatedAt, repoCreatedAt string 218 var repo Repo 219 err := rows.Scan( 220 &issue.ID, 221 &issue.OwnerDid, 222 &issue.RepoAt, 223 &issue.IssueId, 224 &issueCreatedAt, 225 &issue.Title, 226 &issue.Body, 227 &issue.Open, 228 &repo.Did, 229 &repo.Name, 230 &repo.Knot, 231 &repo.Rkey, 232 &repoCreatedAt, 233 ) 234 if err != nil { 235 return nil, err 236 } 237 238 issueCreatedTime, err := time.Parse(time.RFC3339, issueCreatedAt) 239 if err != nil { 240 return nil, err 241 } 242 issue.Created = issueCreatedTime 243 244 repoCreatedTime, err := time.Parse(time.RFC3339, repoCreatedAt) 245 if err != nil { 246 return nil, err 247 } 248 repo.Created = repoCreatedTime 249 250 issue.Metadata = &IssueMetadata{ 251 Repo: &repo, 252 } 253 254 issues = append(issues, issue) 255 } 256 257 if err := rows.Err(); err != nil { 258 return nil, err 259 } 260 261 return issues, nil 262} 263 264func GetIssue(e Execer, repoAt syntax.ATURI, issueId int) (*Issue, error) { 265 query := `select id, owner_did, created, title, body, open from issues where repo_at = ? and issue_id = ?` 266 row := e.QueryRow(query, repoAt, issueId) 267 268 var issue Issue 269 var createdAt string 270 err := row.Scan(&issue.ID, &issue.OwnerDid, &createdAt, &issue.Title, &issue.Body, &issue.Open) 271 if err != nil { 272 return nil, err 273 } 274 275 createdTime, err := time.Parse(time.RFC3339, createdAt) 276 if err != nil { 277 return nil, err 278 } 279 issue.Created = createdTime 280 281 return &issue, nil 282} 283 284func GetIssueWithComments(e Execer, repoAt syntax.ATURI, issueId int) (*Issue, []Comment, error) { 285 query := `select id, owner_did, issue_id, created, title, body, open, issue_at from issues where repo_at = ? and issue_id = ?` 286 row := e.QueryRow(query, repoAt, issueId) 287 288 var issue Issue 289 var createdAt string 290 err := row.Scan(&issue.ID, &issue.OwnerDid, &issue.IssueId, &createdAt, &issue.Title, &issue.Body, &issue.Open, &issue.IssueAt) 291 if err != nil { 292 return nil, nil, err 293 } 294 295 createdTime, err := time.Parse(time.RFC3339, createdAt) 296 if err != nil { 297 return nil, nil, err 298 } 299 issue.Created = createdTime 300 301 comments, err := GetComments(e, repoAt, issueId) 302 if err != nil { 303 return nil, nil, err 304 } 305 306 return &issue, comments, nil 307} 308 309func NewIssueComment(e Execer, comment *Comment) error { 310 query := `insert into comments (owner_did, repo_at, rkey, issue_id, comment_id, body) values (?, ?, ?, ?, ?, ?)` 311 _, err := e.Exec( 312 query, 313 comment.OwnerDid, 314 comment.RepoAt, 315 comment.Rkey, 316 comment.Issue, 317 comment.CommentId, 318 comment.Body, 319 ) 320 return err 321} 322 323func GetComments(e Execer, repoAt syntax.ATURI, issueId int) ([]Comment, error) { 324 var comments []Comment 325 326 rows, err := e.Query(` 327 select 328 owner_did, 329 issue_id, 330 comment_id, 331 rkey, 332 body, 333 created, 334 edited, 335 deleted 336 from 337 comments 338 where 339 repo_at = ? and issue_id = ? 340 order by 341 created asc`, 342 repoAt, 343 issueId, 344 ) 345 if err == sql.ErrNoRows { 346 return []Comment{}, nil 347 } 348 if err != nil { 349 return nil, err 350 } 351 defer rows.Close() 352 353 for rows.Next() { 354 var comment Comment 355 var createdAt string 356 var deletedAt, editedAt, rkey sql.NullString 357 err := rows.Scan(&comment.OwnerDid, &comment.Issue, &comment.CommentId, &rkey, &comment.Body, &createdAt, &editedAt, &deletedAt) 358 if err != nil { 359 return nil, err 360 } 361 362 createdAtTime, err := time.Parse(time.RFC3339, createdAt) 363 if err != nil { 364 return nil, err 365 } 366 comment.Created = &createdAtTime 367 368 if deletedAt.Valid { 369 deletedTime, err := time.Parse(time.RFC3339, deletedAt.String) 370 if err != nil { 371 return nil, err 372 } 373 comment.Deleted = &deletedTime 374 } 375 376 if editedAt.Valid { 377 editedTime, err := time.Parse(time.RFC3339, editedAt.String) 378 if err != nil { 379 return nil, err 380 } 381 comment.Edited = &editedTime 382 } 383 384 if rkey.Valid { 385 comment.Rkey = rkey.String 386 } 387 388 comments = append(comments, comment) 389 } 390 391 if err := rows.Err(); err != nil { 392 return nil, err 393 } 394 395 return comments, nil 396} 397 398func GetComment(e Execer, repoAt syntax.ATURI, issueId, commentId int) (*Comment, error) { 399 query := ` 400 select 401 owner_did, body, rkey, created, deleted, edited 402 from 403 comments where repo_at = ? and issue_id = ? and comment_id = ? 404 ` 405 row := e.QueryRow(query, repoAt, issueId, commentId) 406 407 var comment Comment 408 var createdAt string 409 var deletedAt, editedAt, rkey sql.NullString 410 err := row.Scan(&comment.OwnerDid, &comment.Body, &rkey, &createdAt, &deletedAt, &editedAt) 411 if err != nil { 412 return nil, err 413 } 414 415 createdTime, err := time.Parse(time.RFC3339, createdAt) 416 if err != nil { 417 return nil, err 418 } 419 comment.Created = &createdTime 420 421 if deletedAt.Valid { 422 deletedTime, err := time.Parse(time.RFC3339, deletedAt.String) 423 if err != nil { 424 return nil, err 425 } 426 comment.Deleted = &deletedTime 427 } 428 429 if editedAt.Valid { 430 editedTime, err := time.Parse(time.RFC3339, editedAt.String) 431 if err != nil { 432 return nil, err 433 } 434 comment.Edited = &editedTime 435 } 436 437 if rkey.Valid { 438 comment.Rkey = rkey.String 439 } 440 441 comment.RepoAt = repoAt 442 comment.Issue = issueId 443 comment.CommentId = commentId 444 445 return &comment, nil 446} 447 448func EditComment(e Execer, repoAt syntax.ATURI, issueId, commentId int, newBody string) error { 449 _, err := e.Exec( 450 ` 451 update comments 452 set body = ?, 453 edited = strftime('%Y-%m-%dT%H:%M:%SZ', 'now') 454 where repo_at = ? and issue_id = ? and comment_id = ? 455 `, newBody, repoAt, issueId, commentId) 456 return err 457} 458 459func DeleteComment(e Execer, repoAt syntax.ATURI, issueId, commentId int) error { 460 _, err := e.Exec( 461 ` 462 update comments 463 set body = "", 464 deleted = strftime('%Y-%m-%dT%H:%M:%SZ', 'now') 465 where repo_at = ? and issue_id = ? and comment_id = ? 466 `, repoAt, issueId, commentId) 467 return err 468} 469 470func CloseIssue(e Execer, repoAt syntax.ATURI, issueId int) error { 471 _, err := e.Exec(`update issues set open = 0 where repo_at = ? and issue_id = ?`, repoAt, issueId) 472 return err 473} 474 475func ReopenIssue(e Execer, repoAt syntax.ATURI, issueId int) error { 476 _, err := e.Exec(`update issues set open = 1 where repo_at = ? and issue_id = ?`, repoAt, issueId) 477 return err 478} 479 480type IssueCount struct { 481 Open int 482 Closed int 483} 484 485func GetIssueCount(e Execer, repoAt syntax.ATURI) (IssueCount, error) { 486 row := e.QueryRow(` 487 select 488 count(case when open = 1 then 1 end) as open_count, 489 count(case when open = 0 then 1 end) as closed_count 490 from issues 491 where repo_at = ?`, 492 repoAt, 493 ) 494 495 var count IssueCount 496 if err := row.Scan(&count.Open, &count.Closed); err != nil { 497 return IssueCount{0, 0}, err 498 } 499 500 return count, nil 501}