package repo import ( "context" "database/sql" "errors" "fmt" "strings" "time" ) var ( ErrReleaseNotFound = errors.New("release not found") ErrEmptyReleaseTag = errors.New("release tag cannot be empty") ErrEmptyReleaseTitle = errors.New("release title cannot be empty") ErrReleaseAlreadyExist = errors.New("a release for this tag already exists") ) type Release struct { ID int64 RepoID int64 AuthorID int64 AuthorUsername string AuthorAvatar string TagName string Title string Body string IsDraft bool IsPrerelease bool CreatedAt time.Time PublishedAt time.Time } func (s *Service) CreateRelease(ctx context.Context, repoID, authorID int64, tagName, title, body string, isDraft, isPrerelease bool) (*Release, error) { tagName = strings.TrimSpace(tagName) title = strings.TrimSpace(title) body = strings.TrimSpace(body) if tagName == "" { return nil, ErrEmptyReleaseTag } if title == "" { title = tagName } if len(title) > 200 { title = title[:200] } if len(body) > 50000 { body = body[:50000] } cctx, cancel := context.WithTimeout(ctx, 5*time.Second) defer cancel() b2i := func(b bool) int { if b { return 1 } return 0 } now := time.Now().UTC() res, err := s.db.ExecContext(cctx, ` INSERT INTO releases (repo_id, author_id, tag_name, title, body, is_draft, is_prerelease, created_at, published_at) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?) `, repoID, authorID, tagName, title, body, b2i(isDraft), b2i(isPrerelease), now, now) if err != nil { if strings.Contains(strings.ToLower(err.Error()), "unique") { return nil, ErrReleaseAlreadyExist } return nil, fmt.Errorf("insert release: %w", err) } id, _ := res.LastInsertId() return s.GetReleaseByID(ctx, repoID, id) } func (s *Service) GetRelease(ctx context.Context, repoID int64, tagName string) (*Release, error) { cctx, cancel := context.WithTimeout(ctx, 3*time.Second) defer cancel() var rel Release var draft, prerelease int err := s.db.QueryRowContext(cctx, ` SELECT r.id, r.repo_id, r.author_id, u.username, u.avatar_url, r.tag_name, r.title, r.body, r.is_draft, r.is_prerelease, r.created_at, r.published_at FROM releases r JOIN users u ON u.id = r.author_id WHERE r.repo_id = ? AND r.tag_name = ? `, repoID, tagName).Scan( &rel.ID, &rel.RepoID, &rel.AuthorID, &rel.AuthorUsername, &rel.AuthorAvatar, &rel.TagName, &rel.Title, &rel.Body, &draft, &prerelease, &rel.CreatedAt, &rel.PublishedAt, ) if err != nil { if errors.Is(err, sql.ErrNoRows) { return nil, ErrReleaseNotFound } return nil, fmt.Errorf("get release: %w", err) } rel.IsDraft = draft == 1 rel.IsPrerelease = prerelease == 1 return &rel, nil } func (s *Service) GetReleaseByID(ctx context.Context, repoID, releaseID int64) (*Release, error) { cctx, cancel := context.WithTimeout(ctx, 3*time.Second) defer cancel() var rel Release var draft, prerelease int err := s.db.QueryRowContext(cctx, ` SELECT r.id, r.repo_id, r.author_id, u.username, u.avatar_url, r.tag_name, r.title, r.body, r.is_draft, r.is_prerelease, r.created_at, r.published_at FROM releases r JOIN users u ON u.id = r.author_id WHERE r.repo_id = ? AND r.id = ? `, repoID, releaseID).Scan( &rel.ID, &rel.RepoID, &rel.AuthorID, &rel.AuthorUsername, &rel.AuthorAvatar, &rel.TagName, &rel.Title, &rel.Body, &draft, &prerelease, &rel.CreatedAt, &rel.PublishedAt, ) if err != nil { if errors.Is(err, sql.ErrNoRows) { return nil, ErrReleaseNotFound } return nil, fmt.Errorf("get release: %w", err) } rel.IsDraft = draft == 1 rel.IsPrerelease = prerelease == 1 return &rel, nil } func (s *Service) ListReleases(ctx context.Context, repoID int64) ([]Release, error) { cctx, cancel := context.WithTimeout(ctx, 5*time.Second) defer cancel() rows, err := s.db.QueryContext(cctx, ` SELECT r.id, r.repo_id, r.author_id, u.username, u.avatar_url, r.tag_name, r.title, r.body, r.is_draft, r.is_prerelease, r.created_at, r.published_at FROM releases r JOIN users u ON u.id = r.author_id WHERE r.repo_id = ? ORDER BY r.published_at DESC `, repoID) if err != nil { return nil, fmt.Errorf("list releases: %w", err) } defer rows.Close() var list []Release for rows.Next() { var rel Release var draft, prerelease int if err := rows.Scan( &rel.ID, &rel.RepoID, &rel.AuthorID, &rel.AuthorUsername, &rel.AuthorAvatar, &rel.TagName, &rel.Title, &rel.Body, &draft, &prerelease, &rel.CreatedAt, &rel.PublishedAt, ); err != nil { return nil, fmt.Errorf("scan release: %w", err) } rel.IsDraft = draft == 1 rel.IsPrerelease = prerelease == 1 list = append(list, rel) } return list, rows.Err() } func (s *Service) UpdateRelease(ctx context.Context, releaseID int64, title, body string, isPrerelease bool) error { cctx, cancel := context.WithTimeout(ctx, 3*time.Second) defer cancel() title = strings.TrimSpace(title) body = strings.TrimSpace(body) if len(title) > 200 { title = title[:200] } if len(body) > 50000 { body = body[:50000] } preInt := 0 if isPrerelease { preInt = 1 } _, err := s.db.ExecContext(cctx, ` UPDATE releases SET title = ?, body = ?, is_prerelease = ? WHERE id = ? `, title, body, preInt, releaseID) return err } func (s *Service) DeleteRelease(ctx context.Context, releaseID int64) error { cctx, cancel := context.WithTimeout(ctx, 3*time.Second) defer cancel() _, err := s.db.ExecContext(cctx, `DELETE FROM releases WHERE id = ?`, releaseID) return err }