internal/store/releases.go
168 lines · 4572 bytes
12 symbols in this file
1package store
2
3import (
4 "database/sql"
5 "errors"
6 "fmt"
7)
8
9type Release struct {
10 ID int64
11 RepoID int64
12 Tag string
13 Title string
14 Notes string
15 NotesFormat string // md | org
16 Author string
17 CreatedAt string
18 Assets []ReleaseAsset
19}
20
21type ReleaseAsset struct {
22 ID int64
23 Name string
24 Size int64
25 SHA256 string
26 UploadedAt string
27}
28
29func (s *Store) CreateRelease(repoID int64, tag, title, notes string, authorID int64, format string) (int64, error) {
30 res, err := s.DB.Exec(
31 "INSERT INTO releases (repo_id, tag, title, notes, author_id, notes_format) VALUES (?, ?, ?, ?, ?, ?)",
32 repoID, tag, title, notes, authorID, format)
33 if err != nil {
34 if isUniqueErr(err) {
35 return 0, fmt.Errorf("a release for tag %q already exists", tag)
36 }
37 return 0, err
38 }
39 return res.LastInsertId()
40}
41
42const releaseSelect = `
43 SELECT r.id, r.repo_id, r.tag, r.title, r.notes, r.notes_format, COALESCE(u.username, ''), r.created_at
44 FROM releases r LEFT JOIN users u ON u.id = r.author_id`
45
46func (s *Store) releaseAssets(rel *Release) error {
47 rows, err := s.DB.Query(
48 "SELECT id, name, size, sha256, uploaded_at FROM release_assets WHERE release_id = ? ORDER BY name",
49 rel.ID)
50 if err != nil {
51 return err
52 }
53 defer rows.Close()
54 for rows.Next() {
55 var a ReleaseAsset
56 if err := rows.Scan(&a.ID, &a.Name, &a.Size, &a.SHA256, &a.UploadedAt); err != nil {
57 return err
58 }
59 rel.Assets = append(rel.Assets, a)
60 }
61 return rows.Err()
62}
63
64// UpdateRelease replaces a release's title, notes, and markup format.
65func (s *Store) UpdateRelease(repoID int64, tag, title, notes, format string) error {
66 res, err := s.DB.Exec(
67 "UPDATE releases SET title = ?, notes = ?, notes_format = ? WHERE repo_id = ? AND tag = ?",
68 title, notes, format, repoID, tag)
69 if err != nil {
70 return err
71 }
72 if n, _ := res.RowsAffected(); n == 0 {
73 return ErrNotFound
74 }
75 return nil
76}
77
78func (s *Store) ReleaseByTag(repoID int64, tag string) (Release, error) {
79 var r Release
80 err := s.DB.QueryRow(releaseSelect+" WHERE r.repo_id = ? AND r.tag = ?", repoID, tag).
81 Scan(&r.ID, &r.RepoID, &r.Tag, &r.Title, &r.Notes, &r.NotesFormat, &r.Author, &r.CreatedAt)
82 if errors.Is(err, sql.ErrNoRows) {
83 return r, ErrNotFound
84 }
85 if err != nil {
86 return r, err
87 }
88 return r, s.releaseAssets(&r)
89}
90
91// ListReleases returns releases newest-first, assets included.
92func (s *Store) ListReleases(repoID int64) ([]Release, error) {
93 return s.ListReleasesPage(repoID, 0, "", 0)
94}
95
96// ListReleasesPage lists newest first. limit 0 is every row; afterCreated
97// and afterID are the (created_at, id) of the last release of the
98// previous page, afterID 0 for the first page. The pair, not a lookup by
99// id, is what survives the pointed-to release being deleted between
100// requests.
101func (s *Store) ListReleasesPage(repoID int64, limit int, afterCreated string, afterID int64) ([]Release, error) {
102 q := releaseSelect + " WHERE r.repo_id = ?"
103 args := []any{repoID}
104 if afterID > 0 {
105 q += " AND (r.created_at, r.id) < (?, ?)"
106 args = append(args, afterCreated, afterID)
107 }
108 q += " ORDER BY r.created_at DESC, r.id DESC"
109 if limit > 0 {
110 q += " LIMIT ?"
111 args = append(args, limit)
112 }
113 rows, err := s.DB.Query(q, args...)
114 if err != nil {
115 return nil, err
116 }
117 defer rows.Close()
118 var out []Release
119 for rows.Next() {
120 var r Release
121 if err := rows.Scan(&r.ID, &r.RepoID, &r.Tag, &r.Title, &r.Notes, &r.NotesFormat, &r.Author, &r.CreatedAt); err != nil {
122 return nil, err
123 }
124 out = append(out, r)
125 }
126 if err := rows.Err(); err != nil {
127 return nil, err
128 }
129 for i := range out {
130 if err := s.releaseAssets(&out[i]); err != nil {
131 return nil, err
132 }
133 }
134 return out, nil
135}
136
137func (s *Store) DeleteRelease(id int64) error {
138 res, err := s.DB.Exec("DELETE FROM releases WHERE id = ?", id)
139 if err != nil {
140 return err
141 }
142 if n, _ := res.RowsAffected(); n == 0 {
143 return ErrNotFound
144 }
145 return nil
146}
147
148func (s *Store) AddReleaseAsset(releaseID int64, name string, size int64, sha256 string) error {
149 _, err := s.DB.Exec(
150 "INSERT INTO release_assets (release_id, name, size, sha256) VALUES (?, ?, ?, ?)",
151 releaseID, name, size, sha256)
152 if isUniqueErr(err) {
153 return fmt.Errorf("asset %q already exists on this release", name)
154 }
155 return err
156}
157
158func (s *Store) RemoveReleaseAsset(releaseID int64, name string) error {
159 res, err := s.DB.Exec(
160 "DELETE FROM release_assets WHERE release_id = ? AND name = ?", releaseID, name)
161 if err != nil {
162 return err
163 }
164 if n, _ := res.RowsAffected(); n == 0 {
165 return ErrNotFound
166 }
167 return nil
168}