update_server/internal/db/projects.go
2026-06-10 20:51:17 +03:00

326 lines
7 KiB
Go

package db
import (
"context"
"database/sql"
"errors"
"fmt"
"strings"
)
type CreateProjectParams struct {
Name string
Slug string
Description string
}
type UpdateProjectParams struct {
Name string
Slug string
Description string
}
func (r *ProjectRepository) List(ctx context.Context) ([]ProjectListItem, error) {
rows, err := r.q.QueryContext(
ctx,
`SELECT
p.id,
p.name,
p.slug,
p.description,
p.is_active,
p.created_at,
p.updated_at,
COUNT(DISTINCT pt.tag_id) AS tag_count,
COUNT(DISTINCT rel.id) AS release_count
FROM projects AS p
LEFT JOIN project_tags AS pt ON pt.project_id = p.id
LEFT JOIN releases AS rel ON rel.project_id = p.id AND rel.is_active = 1
GROUP BY p.id
ORDER BY p.is_active DESC, p.updated_at DESC, p.created_at DESC, p.name COLLATE NOCASE`,
)
if err != nil {
return nil, fmt.Errorf("query projects: %w", err)
}
defer rows.Close()
projects := make([]ProjectListItem, 0)
for rows.Next() {
item, err := scanProjectListItem(rows)
if err != nil {
return nil, fmt.Errorf("scan project list item: %w", err)
}
projects = append(projects, *item)
}
if err := rows.Err(); err != nil {
return nil, fmt.Errorf("iterate projects: %w", err)
}
return projects, nil
}
func (r *ProjectRepository) GetByID(ctx context.Context, id int64) (*Project, error) {
project, err := scanProject(r.q.QueryRowContext(
ctx,
`SELECT id, name, slug, description, is_active, created_at, updated_at
FROM projects
WHERE id = ?`,
id,
))
if err != nil {
if errors.Is(err, sql.ErrNoRows) {
return nil, ErrNotFound
}
return nil, fmt.Errorf("scan project by id: %w", err)
}
return project, nil
}
func (r *ProjectRepository) GetBySlug(ctx context.Context, slug string) (*Project, error) {
project, err := scanProject(r.q.QueryRowContext(
ctx,
`SELECT id, name, slug, description, is_active, created_at, updated_at
FROM projects
WHERE slug = ?
LIMIT 1`,
strings.TrimSpace(slug),
))
if err != nil {
if errors.Is(err, sql.ErrNoRows) {
return nil, ErrNotFound
}
return nil, fmt.Errorf("scan project by slug: %w", err)
}
return project, nil
}
func (r *ProjectRepository) Create(ctx context.Context, params CreateProjectParams) (*Project, error) {
result, err := r.q.ExecContext(
ctx,
`INSERT INTO projects (name, slug, description) VALUES (?, ?, ?)`,
strings.TrimSpace(params.Name),
strings.TrimSpace(params.Slug),
strings.TrimSpace(params.Description),
)
if err != nil {
if isUniqueConstraintError(err) {
return nil, errors.Join(ErrConflict, fmt.Errorf("insert project: %w", err))
}
return nil, fmt.Errorf("insert project: %w", err)
}
projectID, err := result.LastInsertId()
if err != nil {
return nil, fmt.Errorf("load inserted project id: %w", err)
}
return r.GetByID(ctx, projectID)
}
func (r *ProjectRepository) Update(ctx context.Context, projectID int64, params UpdateProjectParams) (*Project, error) {
result, err := r.q.ExecContext(
ctx,
`UPDATE projects
SET name = ?, slug = ?, description = ?
WHERE id = ?`,
strings.TrimSpace(params.Name),
strings.TrimSpace(params.Slug),
strings.TrimSpace(params.Description),
projectID,
)
if err != nil {
if isUniqueConstraintError(err) {
return nil, errors.Join(ErrConflict, fmt.Errorf("update project: %w", err))
}
return nil, fmt.Errorf("update project: %w", err)
}
rowsAffected, err := result.RowsAffected()
if err != nil {
return nil, fmt.Errorf("read updated project rows: %w", err)
}
if rowsAffected == 0 {
return nil, ErrNotFound
}
return r.GetByID(ctx, projectID)
}
func (r *ProjectRepository) SetActive(ctx context.Context, projectID int64, isActive bool) error {
activeValue := 0
if isActive {
activeValue = 1
}
result, err := r.q.ExecContext(
ctx,
`UPDATE projects
SET is_active = ?
WHERE id = ?`,
activeValue,
projectID,
)
if err != nil {
return fmt.Errorf("update project active state: %w", err)
}
rowsAffected, err := result.RowsAffected()
if err != nil {
return fmt.Errorf("read affected project rows: %w", err)
}
if rowsAffected == 0 {
return ErrNotFound
}
return nil
}
func (r *ProjectRepository) ListTags(ctx context.Context, projectID int64) ([]Tag, error) {
rows, err := r.q.QueryContext(
ctx,
`SELECT
t.id,
t.name,
t.slug,
t.description,
t.created_at,
t.updated_at
FROM tags AS t
INNER JOIN project_tags AS pt ON pt.tag_id = t.id
WHERE pt.project_id = ?
ORDER BY t.name COLLATE NOCASE`,
projectID,
)
if err != nil {
return nil, fmt.Errorf("query project tags: %w", err)
}
defer rows.Close()
tags := make([]Tag, 0)
for rows.Next() {
tag, err := scanTag(rows)
if err != nil {
return nil, fmt.Errorf("scan project tag: %w", err)
}
tags = append(tags, *tag)
}
if err := rows.Err(); err != nil {
return nil, fmt.Errorf("iterate project tags: %w", err)
}
return tags, nil
}
func (r *ProjectRepository) AttachTag(ctx context.Context, projectID, tagID int64) error {
if _, err := r.q.ExecContext(
ctx,
`INSERT OR IGNORE INTO project_tags (project_id, tag_id) VALUES (?, ?)`,
projectID,
tagID,
); err != nil {
return fmt.Errorf("insert project tag link: %w", err)
}
return nil
}
func (r *ProjectRepository) DetachTag(ctx context.Context, projectID, tagID int64) error {
if _, err := r.q.ExecContext(
ctx,
`DELETE FROM project_tags WHERE project_id = ? AND tag_id = ?`,
projectID,
tagID,
); err != nil {
return fmt.Errorf("delete project tag link: %w", err)
}
return nil
}
func scanProject(scanner rowScanner) (*Project, error) {
var (
project Project
isActive int
createdAtRaw string
updatedAtRaw string
)
if err := scanner.Scan(
&project.ID,
&project.Name,
&project.Slug,
&project.Description,
&isActive,
&createdAtRaw,
&updatedAtRaw,
); err != nil {
return nil, err
}
createdAt, err := parseTimestamp(createdAtRaw)
if err != nil {
return nil, fmt.Errorf("parse project created_at: %w", err)
}
updatedAt, err := parseTimestamp(updatedAtRaw)
if err != nil {
return nil, fmt.Errorf("parse project updated_at: %w", err)
}
project.IsActive = isActive == 1
project.CreatedAt = createdAt
project.UpdatedAt = updatedAt
return &project, nil
}
func scanProjectListItem(scanner rowScanner) (*ProjectListItem, error) {
var (
item ProjectListItem
isActive int
createdAtRaw string
updatedAtRaw string
)
if err := scanner.Scan(
&item.Project.ID,
&item.Project.Name,
&item.Project.Slug,
&item.Project.Description,
&isActive,
&createdAtRaw,
&updatedAtRaw,
&item.TagCount,
&item.ReleaseCount,
); err != nil {
return nil, err
}
createdAt, err := parseTimestamp(createdAtRaw)
if err != nil {
return nil, fmt.Errorf("parse project list created_at: %w", err)
}
updatedAt, err := parseTimestamp(updatedAtRaw)
if err != nil {
return nil, fmt.Errorf("parse project list updated_at: %w", err)
}
item.Project.IsActive = isActive == 1
item.Project.CreatedAt = createdAt
item.Project.UpdatedAt = updatedAt
return &item, nil
}