This commit is contained in:
delete 2026-06-10 20:51:17 +03:00
commit b15b95781c
108 changed files with 14802 additions and 0 deletions

285
internal/db/releases.go Normal file
View file

@ -0,0 +1,285 @@
package db
import (
"context"
"database/sql"
"errors"
"fmt"
"strings"
)
type CreateReleaseParams struct {
ProjectID int64
Version string
Build string
Filename string
StoragePath string
ChecksumSHA256 string
SizeBytes int64
ContentType string
ReleaseNotes string
UploadedByUserID *int64
IsActive bool
}
func (r *ReleaseRepository) Create(ctx context.Context, params CreateReleaseParams) (*Release, error) {
isActive := 0
if params.IsActive {
isActive = 1
}
result, err := r.q.ExecContext(
ctx,
`INSERT INTO releases (
project_id,
version,
build,
filename,
storage_path,
checksum_sha256,
size_bytes,
content_type,
release_notes,
uploaded_by_user_id,
is_active
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
params.ProjectID,
strings.TrimSpace(params.Version),
strings.TrimSpace(params.Build),
strings.TrimSpace(params.Filename),
strings.TrimSpace(params.StoragePath),
strings.TrimSpace(params.ChecksumSHA256),
params.SizeBytes,
strings.TrimSpace(params.ContentType),
strings.TrimSpace(params.ReleaseNotes),
params.UploadedByUserID,
isActive,
)
if err != nil {
if isUniqueConstraintError(err) {
return nil, errors.Join(ErrConflict, fmt.Errorf("insert release: %w", err))
}
return nil, fmt.Errorf("insert release: %w", err)
}
releaseID, err := result.LastInsertId()
if err != nil {
return nil, fmt.Errorf("load inserted release id: %w", err)
}
return r.GetByID(ctx, releaseID)
}
func (r *ReleaseRepository) GetByID(ctx context.Context, id int64) (*Release, error) {
release, err := scanRelease(r.q.QueryRowContext(
ctx,
`SELECT
id,
project_id,
version,
build,
filename,
storage_path,
checksum_sha256,
size_bytes,
content_type,
release_notes,
created_at,
updated_at,
uploaded_by_user_id,
is_active
FROM releases
WHERE id = ?`,
id,
))
if err != nil {
if errors.Is(err, sql.ErrNoRows) {
return nil, ErrNotFound
}
return nil, fmt.Errorf("scan release by id: %w", err)
}
return release, nil
}
func (r *ReleaseRepository) GetLatestByProjectID(ctx context.Context, projectID int64) (*Release, error) {
release, err := scanRelease(r.q.QueryRowContext(
ctx,
`SELECT
id,
project_id,
version,
build,
filename,
storage_path,
checksum_sha256,
size_bytes,
content_type,
release_notes,
created_at,
updated_at,
uploaded_by_user_id,
is_active
FROM releases
WHERE project_id = ?
AND is_active = 1
ORDER BY created_at DESC, id DESC
LIMIT 1`,
projectID,
))
if err != nil {
if errors.Is(err, sql.ErrNoRows) {
return nil, ErrNotFound
}
return nil, fmt.Errorf("scan latest release by project id: %w", err)
}
return release, nil
}
func (r *ReleaseRepository) ListByProjectID(ctx context.Context, projectID int64) ([]ReleaseListItem, error) {
rows, err := r.q.QueryContext(
ctx,
`SELECT
r.id,
r.project_id,
r.version,
r.build,
r.filename,
r.storage_path,
r.checksum_sha256,
r.size_bytes,
r.content_type,
r.release_notes,
r.created_at,
r.updated_at,
r.uploaded_by_user_id,
r.is_active,
COALESCE(u.email, '')
FROM releases AS r
LEFT JOIN users AS u ON u.id = r.uploaded_by_user_id
WHERE r.project_id = ?
ORDER BY r.created_at DESC, r.id DESC`,
projectID,
)
if err != nil {
return nil, fmt.Errorf("query project releases: %w", err)
}
defer rows.Close()
releases := make([]ReleaseListItem, 0)
for rows.Next() {
item, err := scanReleaseListItem(rows)
if err != nil {
return nil, fmt.Errorf("scan project release: %w", err)
}
releases = append(releases, *item)
}
if err := rows.Err(); err != nil {
return nil, fmt.Errorf("iterate project releases: %w", err)
}
return releases, nil
}
func scanRelease(scanner rowScanner) (*Release, error) {
var (
release Release
createdAtRaw string
updatedAtRaw string
uploadedByUserID sql.NullInt64
isActive int
)
if err := scanner.Scan(
&release.ID,
&release.ProjectID,
&release.Version,
&release.Build,
&release.Filename,
&release.StoragePath,
&release.ChecksumSHA256,
&release.SizeBytes,
&release.ContentType,
&release.ReleaseNotes,
&createdAtRaw,
&updatedAtRaw,
&uploadedByUserID,
&isActive,
); err != nil {
return nil, err
}
createdAt, err := parseTimestamp(createdAtRaw)
if err != nil {
return nil, fmt.Errorf("parse release created_at: %w", err)
}
updatedAt, err := parseTimestamp(updatedAtRaw)
if err != nil {
return nil, fmt.Errorf("parse release updated_at: %w", err)
}
release.CreatedAt = createdAt
release.UpdatedAt = updatedAt
release.IsActive = isActive == 1
if uploadedByUserID.Valid {
release.UploadedByUserID = &uploadedByUserID.Int64
}
return &release, nil
}
func scanReleaseListItem(scanner rowScanner) (*ReleaseListItem, error) {
var (
item ReleaseListItem
createdAtRaw string
updatedAtRaw string
uploadedByUserID sql.NullInt64
isActive int
)
if err := scanner.Scan(
&item.Release.ID,
&item.Release.ProjectID,
&item.Release.Version,
&item.Release.Build,
&item.Release.Filename,
&item.Release.StoragePath,
&item.Release.ChecksumSHA256,
&item.Release.SizeBytes,
&item.Release.ContentType,
&item.Release.ReleaseNotes,
&createdAtRaw,
&updatedAtRaw,
&uploadedByUserID,
&isActive,
&item.UploadedByEmail,
); err != nil {
return nil, err
}
createdAt, err := parseTimestamp(createdAtRaw)
if err != nil {
return nil, fmt.Errorf("parse release list created_at: %w", err)
}
updatedAt, err := parseTimestamp(updatedAtRaw)
if err != nil {
return nil, fmt.Errorf("parse release list updated_at: %w", err)
}
item.Release.CreatedAt = createdAt
item.Release.UpdatedAt = updatedAt
item.Release.IsActive = isActive == 1
if uploadedByUserID.Valid {
item.Release.UploadedByUserID = &uploadedByUserID.Int64
}
return &item, nil
}