init
This commit is contained in:
commit
b15b95781c
108 changed files with 14802 additions and 0 deletions
326
internal/db/projects.go
Normal file
326
internal/db/projects.go
Normal file
|
|
@ -0,0 +1,326 @@
|
|||
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
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue