2026-09-27 19:42:49 +03:00
|
|
|
package store
|
|
|
|
|
|
|
|
|
|
import (
|
|
|
|
|
"context"
|
|
|
|
|
"errors"
|
|
|
|
|
|
2026-09-27 20:17:38 +03:00
|
|
|
"github.com/jackc/pgx/v5/pgconn"
|
2026-09-27 19:42:49 +03:00
|
|
|
"github.com/jackc/pgerrcode"
|
|
|
|
|
"github.com/jackc/pgx/v5"
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
// CreateUser inserts a new user returning the created record or ErrDuplicate on conflict.
|
|
|
|
|
func (s *Store) CreateUser(ctx context.Context, username, displayName, passwordHash string) (*User, error) {
|
|
|
|
|
var u User
|
|
|
|
|
err := s.pool.QueryRow(ctx, `
|
|
|
|
|
INSERT INTO users(username, display_name, password_hash)
|
|
|
|
|
VALUES($1, $2, $3)
|
|
|
|
|
RETURNING id, username, display_name, password_hash, avatar, created_at
|
|
|
|
|
`, username, displayName, passwordHash).Scan(
|
|
|
|
|
&u.ID, &u.Username, &u.DisplayName, &u.PasswordHash, &u.Avatar, &u.CreatedAt,
|
|
|
|
|
)
|
|
|
|
|
if err != nil {
|
|
|
|
|
var pgErr *pgconn.PgError
|
|
|
|
|
if errors.As(err, &pgErr) && pgErr.Code == pgerrcode.UniqueViolation {
|
|
|
|
|
return nil, ErrDuplicate
|
|
|
|
|
}
|
|
|
|
|
return nil, err
|
|
|
|
|
}
|
|
|
|
|
return &u, nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// GetUserByUsername retrieves a user by username or returns ErrNotFound.
|
|
|
|
|
func (s *Store) GetUserByUsername(ctx context.Context, username string) (*User, error) {
|
|
|
|
|
var u User
|
|
|
|
|
err := s.pool.QueryRow(ctx, `
|
|
|
|
|
SELECT id, username, display_name, password_hash, avatar, created_at
|
|
|
|
|
FROM users WHERE username=$1
|
|
|
|
|
`, username).Scan(
|
|
|
|
|
&u.ID, &u.Username, &u.DisplayName, &u.PasswordHash, &u.Avatar, &u.CreatedAt,
|
|
|
|
|
)
|
|
|
|
|
if err != nil {
|
|
|
|
|
if errors.Is(err, pgx.ErrNoRows) {
|
|
|
|
|
return nil, ErrNotFound
|
|
|
|
|
}
|
|
|
|
|
return nil, err
|
|
|
|
|
}
|
|
|
|
|
return &u, nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// GetUserByID retrieves a user by ID or returns ErrNotFound.
|
|
|
|
|
func (s *Store) GetUserByID(ctx context.Context, id int64) (*User, error) {
|
|
|
|
|
var u User
|
|
|
|
|
err := s.pool.QueryRow(ctx, `
|
|
|
|
|
SELECT id, username, display_name, password_hash, avatar, created_at
|
|
|
|
|
FROM users WHERE id=$1
|
|
|
|
|
`, id).Scan(
|
|
|
|
|
&u.ID, &u.Username, &u.DisplayName, &u.PasswordHash, &u.Avatar, &u.CreatedAt,
|
|
|
|
|
)
|
|
|
|
|
if err != nil {
|
|
|
|
|
if errors.Is(err, pgx.ErrNoRows) {
|
|
|
|
|
return nil, ErrNotFound
|
|
|
|
|
}
|
|
|
|
|
return nil, err
|
|
|
|
|
}
|
|
|
|
|
return &u, nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// SearchUsers returns users matching query in username or display_name via ILIKE.
|
|
|
|
|
// Empty query returns an empty slice.
|
|
|
|
|
func (s *Store) SearchUsers(ctx context.Context, query string, limit int) ([]User, error) {
|
|
|
|
|
if query == "" {
|
|
|
|
|
return []User{}, nil
|
|
|
|
|
}
|
|
|
|
|
pattern := "%" + query + "%"
|
|
|
|
|
rows, err := s.pool.Query(ctx, `
|
|
|
|
|
SELECT id, username, display_name, password_hash, avatar, created_at
|
|
|
|
|
FROM users
|
|
|
|
|
WHERE username ILIKE $1 OR display_name ILIKE $1
|
|
|
|
|
ORDER BY username
|
|
|
|
|
LIMIT $2
|
|
|
|
|
`, pattern, limit)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return nil, err
|
|
|
|
|
}
|
|
|
|
|
defer rows.Close()
|
|
|
|
|
|
|
|
|
|
var users []User
|
|
|
|
|
for rows.Next() {
|
|
|
|
|
var u User
|
|
|
|
|
if err := rows.Scan(
|
|
|
|
|
&u.ID, &u.Username, &u.DisplayName, &u.PasswordHash, &u.Avatar, &u.CreatedAt,
|
|
|
|
|
); err != nil {
|
|
|
|
|
return nil, err
|
|
|
|
|
}
|
|
|
|
|
users = append(users, u)
|
|
|
|
|
}
|
|
|
|
|
return users, nil
|
|
|
|
|
}
|