internal/store/releases.go

ba0a7d33f3a65ce53aafb074fda1682cf1cecfdf
gitbay/internal/store/releases.go history · blame · raw

168 lines · 4572 bytes

  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}