package scenarios_repo import ( "context" "errors" "evening_detective_server/internal/repos" "github.com/jackc/pgx/v5" "github.com/jackc/pgx/v5/pgxpool" ) var ( ErrScenarioNotFound = errors.New("Сценарий не найден") ) type ScenariosRepo struct { pool *pgxpool.Pool } func NewScenariosRepo(pool *pgxpool.Pool) *ScenariosRepo { return &ScenariosRepo{ pool: pool, } } func (r *ScenariosRepo) AddScenario( ctx context.Context, name string, authorId int, ) (int, error) { id := 0 err := r.pool.QueryRow( ctx, `INSERT INTO scenarios (name, author_id) VALUES ($1, $2) RETURNING id`, name, authorId, ).Scan(&id) if err != nil { return 0, err } return id, nil } func (r *ScenariosRepo) GetScenariosByAuthorID( ctx context.Context, authorId int, ) ([]*repos.Scenario, error) { rows, err := r.pool.Query( ctx, `SELECT scenarios.id, scenarios.name, scenarios.description, scenarios.image, scenarios.scenario, users.id, users.username, scenarios.status, scenarios.updated_at, scenarios.created_at, scenarios.published_at FROM scenarios LEFT JOIN users on scenarios.author_id = users.id WHERE scenarios.author_id = $1 and is_deleted = FALSE ORDER BY created_at DESC`, authorId, ) if err != nil { return nil, err } defer rows.Close() var scenarios []*repos.Scenario for rows.Next() { scenario := &repos.Scenario{} var authorID *int var authorUsername *string err := rows.Scan( &scenario.ID, &scenario.Name, &scenario.Description, &scenario.Image, &scenario.Scenario, &authorID, &authorUsername, &scenario.Status, &scenario.UpdatedAt, &scenario.CreatedAt, &scenario.PublishedAt, ) if err != nil { return nil, err } if authorID != nil && authorUsername != nil { scenario.Author = &repos.User{ID: *authorID, Username: *authorUsername} } scenarios = append(scenarios, scenario) } if err := rows.Err(); err != nil { return nil, err } return scenarios, nil } func (r *ScenariosRepo) GetScenariosByStatus( ctx context.Context, status string, ) ([]*repos.Scenario, error) { rows, err := r.pool.Query( ctx, `SELECT scenarios.id, scenarios.name, scenarios.description, scenarios.image, scenarios.scenario, users.id, users.username, scenarios.status, scenarios.updated_at, scenarios.created_at, scenarios.published_at FROM scenarios LEFT JOIN users on scenarios.author_id = users.id WHERE scenarios.status = $1 and is_deleted = FALSE ORDER BY created_at DESC`, status, ) if err != nil { return nil, err } defer rows.Close() var scenarios []*repos.Scenario for rows.Next() { scenario := &repos.Scenario{} var authorID *int var authorUsername *string err := rows.Scan( &scenario.ID, &scenario.Name, &scenario.Description, &scenario.Image, &scenario.Scenario, &authorID, &authorUsername, &scenario.Status, &scenario.UpdatedAt, &scenario.CreatedAt, &scenario.PublishedAt, ) if err != nil { return nil, err } if authorID != nil && authorUsername != nil { scenario.Author = &repos.User{ID: *authorID, Username: *authorUsername} } scenarios = append(scenarios, scenario) } if err := rows.Err(); err != nil { return nil, err } return scenarios, nil } func (r *ScenariosRepo) GetScenarioByID( ctx context.Context, id int, ) (*repos.Scenario, error) { scenario := &repos.Scenario{} var authorID *int var authorUsername *string row := r.pool.QueryRow( ctx, `SELECT scenarios.id, scenarios.name, scenarios.description, scenarios.image, scenarios.scenario, users.id, users.username, scenarios.status, scenarios.updated_at, scenarios.created_at, scenarios.published_at, scenarios.is_deleted FROM scenarios LEFT JOIN users on scenarios.author_id = users.id WHERE scenarios.id = $1`, id, ) err := row.Scan( &scenario.ID, &scenario.Name, &scenario.Description, &scenario.Image, &scenario.Scenario, &authorID, &authorUsername, &scenario.Status, &scenario.UpdatedAt, &scenario.CreatedAt, &scenario.PublishedAt, &scenario.IsDeleted, ) if err != nil { if errors.Is(err, pgx.ErrNoRows) { return nil, ErrScenarioNotFound } return nil, err } // Автор может отсутствовать (обезличен после удаления аккаунта). if authorID != nil && authorUsername != nil { scenario.Author = &repos.User{ID: *authorID, Username: *authorUsername} } return scenario, nil } // UpdateScenarioByID меняет основные поля сценария. func (r *ScenariosRepo) UpdateScenarioByID( ctx context.Context, id int, name string, description string, image string, ) error { tag, err := r.pool.Exec( ctx, `UPDATE scenarios SET name = $1, description = $2, image = $3, updated_at = NOW() WHERE id = $4`, name, description, image, id, ) if err != nil { return err } if tag.RowsAffected() == 0 { return ErrScenarioNotFound } return nil } // UpdateScenarioStatusByID меняет статус сценария. allowPublished=true // разрешает менять статус опубликованного сценария (только для админа). // Возвращает false, если строка не обновлена (например, не-админ пытается // снять с публикации опубликованный сценарий). published_at фиксируется // только при ПЕРВОЙ публикации: повторная публикация не двигает «момент // отчуждения» исключительного права (п. 7.2 Пользовательского соглашения). func (r *ScenariosRepo) UpdateScenarioStatusByID( ctx context.Context, id int, status string, allowPublished bool, ) (bool, error) { tag, err := r.pool.Exec( ctx, `UPDATE scenarios SET status = $1, published_at = CASE WHEN $1 = 'public' AND published_at IS NULL THEN NOW() ELSE published_at END, updated_at = NOW() WHERE id = $2 AND ($3 OR status <> 'public')`, status, id, allowPublished, ) if err != nil { return false, err } return tag.RowsAffected() > 0, nil } // DeleteScenarioByID помечает сценарий удалённым (soft delete). // allowPublished=true разрешает удаление опубликованного сценария // (только для админа). Возвращает false, если строка не обновлена // (например, не-админ пытается удалить опубликованный сценарий). func (r *ScenariosRepo) DeleteScenarioByID( ctx context.Context, id int, allowPublished bool, ) (bool, error) { tag, err := r.pool.Exec( ctx, `UPDATE scenarios SET is_deleted = TRUE WHERE id = $1 AND ($2 OR status <> 'public')`, id, allowPublished, ) if err != nil { return false, err } return tag.RowsAffected() > 0, nil } func (r *ScenariosRepo) GetStoryByScenarioID( ctx context.Context, id int, ) (string, error) { story := "" row := r.pool.QueryRow( ctx, `SELECT scenario FROM scenarios WHERE scenarios.id = $1`, id, ) err := row.Scan( &story, ) if err != nil { if errors.Is(err, pgx.ErrNoRows) { return "", ErrScenarioNotFound } return "", err } return story, nil } // UpdateStoryByScenarioID обновляет содержимое (историю) сценария. func (r *ScenariosRepo) UpdateStoryByScenarioID( ctx context.Context, id int, story string, ) error { tag, err := r.pool.Exec( ctx, `UPDATE scenarios SET scenario = $1, updated_at = NOW() WHERE id = $2`, story, id, ) if err != nil { return err } if tag.RowsAffected() == 0 { return ErrScenarioNotFound } return nil }