generated from VLADIMIR/template
257 lines
5.8 KiB
Go
257 lines
5.8 KiB
Go
// Package tasks_repo persists developer tasks.
|
|
package tasks_repo
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"time"
|
|
|
|
"github.com/jackc/pgx/v5"
|
|
"github.com/jackc/pgx/v5/pgconn"
|
|
"github.com/jackc/pgx/v5/pgxpool"
|
|
)
|
|
|
|
var (
|
|
ErrInvalidTask = errors.New("некорректные поля задачи")
|
|
ErrNotFound = errors.New("задача не найдена")
|
|
ErrConflict = errors.New("задача изменена другим пользователем; обновите данные")
|
|
ErrInvalidAssignee = errors.New("ответственный должен иметь роль разработчика")
|
|
)
|
|
|
|
// Task is a persisted developer task.
|
|
type Task struct {
|
|
ID int
|
|
Title string
|
|
Description string
|
|
Status string
|
|
AssigneeID *int
|
|
AssigneeName string
|
|
AuthorID *int
|
|
DueDate *time.Time
|
|
CreatedAt time.Time
|
|
UpdatedAt time.Time
|
|
Version int
|
|
}
|
|
|
|
// Assignee contains only the public identity needed for task assignment.
|
|
type Assignee struct {
|
|
ID int
|
|
Name string
|
|
}
|
|
|
|
// TasksRepo persists the shared task board.
|
|
type TasksRepo struct{ pool *pgxpool.Pool }
|
|
|
|
func NewTasksRepo(pool *pgxpool.Pool) *TasksRepo { return &TasksRepo{pool: pool} }
|
|
|
|
const taskColumns = `
|
|
id,
|
|
title,
|
|
description,
|
|
status,
|
|
assignee_id,
|
|
author_id,
|
|
due_date,
|
|
created_at,
|
|
updated_at,
|
|
version,
|
|
COALESCE((
|
|
SELECT username
|
|
FROM users
|
|
WHERE users.id = developer_tasks.assignee_id
|
|
), '')`
|
|
|
|
func scanTask(row pgx.Row) (*Task, error) {
|
|
var task Task
|
|
err := row.Scan(&task.ID, &task.Title, &task.Description, &task.Status,
|
|
&task.AssigneeID, &task.AuthorID, &task.DueDate, &task.CreatedAt, &task.UpdatedAt, &task.Version, &task.AssigneeName)
|
|
if errors.Is(err, pgx.ErrNoRows) {
|
|
return nil, ErrNotFound
|
|
}
|
|
return &task, err
|
|
}
|
|
|
|
func (r *TasksRepo) Get(ctx context.Context, id int) (*Task, error) {
|
|
return scanTask(r.pool.QueryRow(
|
|
ctx,
|
|
`SELECT `+taskColumns+`
|
|
FROM developer_tasks
|
|
WHERE id = $1`,
|
|
id,
|
|
))
|
|
}
|
|
|
|
func (r *TasksRepo) List(ctx context.Context) ([]*Task, error) {
|
|
rows, err := r.pool.Query(
|
|
ctx,
|
|
`SELECT
|
|
t.id,
|
|
t.title,
|
|
t.description,
|
|
t.status,
|
|
t.assignee_id,
|
|
t.author_id,
|
|
t.due_date,
|
|
t.created_at,
|
|
t.updated_at,
|
|
t.version,
|
|
COALESCE(u.username, '')
|
|
FROM developer_tasks t
|
|
LEFT JOIN users u ON u.id = t.assignee_id
|
|
ORDER BY t.created_at DESC, t.id DESC`,
|
|
)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer rows.Close()
|
|
tasks := make([]*Task, 0)
|
|
for rows.Next() {
|
|
var task Task
|
|
if err := rows.Scan(&task.ID, &task.Title, &task.Description, &task.Status,
|
|
&task.AssigneeID, &task.AuthorID, &task.DueDate, &task.CreatedAt,
|
|
&task.UpdatedAt, &task.Version, &task.AssigneeName); err != nil {
|
|
return nil, err
|
|
}
|
|
tasks = append(tasks, &task)
|
|
}
|
|
return tasks, rows.Err()
|
|
}
|
|
|
|
func (r *TasksRepo) ListAssignees(ctx context.Context) ([]Assignee, error) {
|
|
rows, err := r.pool.Query(
|
|
ctx,
|
|
`SELECT
|
|
id,
|
|
username
|
|
FROM users
|
|
WHERE roles @> '["developer"]'::jsonb
|
|
ORDER BY username, id`,
|
|
)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer rows.Close()
|
|
result := make([]Assignee, 0)
|
|
for rows.Next() {
|
|
var user Assignee
|
|
if err := rows.Scan(&user.ID, &user.Name); err != nil {
|
|
return nil, err
|
|
}
|
|
result = append(result, user)
|
|
}
|
|
return result, rows.Err()
|
|
}
|
|
|
|
func (r *TasksRepo) Create(ctx context.Context, task Task) (*Task, error) {
|
|
result, err := scanTask(r.pool.QueryRow(
|
|
ctx,
|
|
`INSERT INTO developer_tasks (title, description, status, assignee_id, author_id, due_date)
|
|
SELECT $1, $2, $3, $4::integer, $5::integer, $6::date
|
|
WHERE $4::integer IS NULL OR EXISTS (
|
|
SELECT 1
|
|
FROM users
|
|
WHERE id = $4 AND roles @> '["developer"]'::jsonb
|
|
)
|
|
RETURNING `+taskColumns,
|
|
task.Title,
|
|
task.Description,
|
|
task.Status,
|
|
task.AssigneeID,
|
|
task.AuthorID,
|
|
task.DueDate,
|
|
))
|
|
if errors.Is(err, ErrNotFound) {
|
|
return nil, ErrInvalidAssignee
|
|
}
|
|
return result, writeError(err)
|
|
}
|
|
|
|
func (r *TasksRepo) Update(ctx context.Context, task Task) (*Task, error) {
|
|
result, err := scanTask(r.pool.QueryRow(
|
|
ctx,
|
|
`UPDATE developer_tasks
|
|
SET title = $1, description = $2, status = $3,
|
|
assignee_id = $4, due_date = $5, updated_at = NOW(), version = version + 1
|
|
WHERE id = $6 AND version = $7 AND (
|
|
$4::integer IS NULL OR assignee_id = $4 OR EXISTS (
|
|
SELECT 1
|
|
FROM users
|
|
WHERE id = $4 AND roles @> '["developer"]'::jsonb
|
|
)
|
|
)
|
|
RETURNING `+taskColumns,
|
|
task.Title,
|
|
task.Description,
|
|
task.Status,
|
|
task.AssigneeID,
|
|
task.DueDate,
|
|
task.ID,
|
|
task.Version,
|
|
))
|
|
if errors.Is(err, ErrNotFound) {
|
|
current, getErr := r.Get(ctx, task.ID)
|
|
if getErr != nil {
|
|
return nil, getErr
|
|
}
|
|
if current.Version != task.Version {
|
|
return nil, ErrConflict
|
|
}
|
|
return nil, ErrInvalidAssignee
|
|
}
|
|
return result, writeError(err)
|
|
}
|
|
|
|
func (r *TasksRepo) SetStatus(ctx context.Context, id, version int, status string) (*Task, error) {
|
|
result, err := scanTask(r.pool.QueryRow(
|
|
ctx,
|
|
`UPDATE developer_tasks
|
|
SET status = $1, updated_at = NOW(), version = version + 1
|
|
WHERE id = $2 AND version = $3
|
|
RETURNING `+taskColumns,
|
|
status,
|
|
id,
|
|
version,
|
|
))
|
|
if errors.Is(err, ErrNotFound) {
|
|
return nil, r.missingWrite(ctx, id)
|
|
}
|
|
return result, err
|
|
}
|
|
|
|
func (r *TasksRepo) Delete(ctx context.Context, id, version int) error {
|
|
tag, err := r.pool.Exec(
|
|
ctx,
|
|
`DELETE FROM developer_tasks
|
|
WHERE id = $1 AND version = $2`,
|
|
id,
|
|
version,
|
|
)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if tag.RowsAffected() == 0 {
|
|
return r.missingWrite(ctx, id)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (r *TasksRepo) missingWrite(ctx context.Context, id int) error {
|
|
if _, err := r.Get(ctx, id); err != nil {
|
|
return err
|
|
}
|
|
return ErrConflict
|
|
}
|
|
|
|
func writeError(err error) error {
|
|
var pgErr *pgconn.PgError
|
|
if errors.As(err, &pgErr) {
|
|
switch pgErr.Code {
|
|
case "23503":
|
|
return ErrInvalidAssignee
|
|
case "23514", "22001":
|
|
return ErrInvalidTask
|
|
}
|
|
}
|
|
return err
|
|
}
|