xtablo-source/go-backend/internal/db/repository.go
2026-05-09 20:18:24 +02:00

226 lines
5.7 KiB
Go

package db
import (
"context"
"errors"
"fmt"
"strings"
"time"
"github.com/google/uuid"
"github.com/jackc/pgx/v5"
"github.com/jackc/pgx/v5/pgconn"
"github.com/jackc/pgx/v5/pgtype"
"github.com/jackc/pgx/v5/pgxpool"
"github.com/rs/zerolog/log"
sqlcdb "xtablo-backend/internal/db/sqlc"
tablomodel "xtablo-backend/internal/tablos"
"xtablo-backend/internal/web/handlers"
)
type PostgresAuthRepository struct {
pool *pgxpool.Pool
queries *sqlcdb.Queries
}
func NewPostgresAuthRepository(ctx context.Context, databaseURL string) (*PostgresAuthRepository, error) {
if databaseURL == "" {
return nil, errors.New("DATABASE_URL is required")
}
pool, err := pgxpool.New(ctx, databaseURL)
if err != nil {
return nil, fmt.Errorf("connect postgres: %w", err)
}
return &PostgresAuthRepository{
pool: pool,
queries: sqlcdb.New(pool),
}, nil
}
func (r *PostgresAuthRepository) Close() {
r.pool.Close()
}
func (r *PostgresAuthRepository) CreateAuthUser(ctx context.Context, input handlers.CreateAuthUserInput) (uuid.UUID, error) {
id := uuid.New()
createdID, err := r.queries.CreateAuthUser(ctx, sqlcdb.CreateAuthUserParams{
ID: id,
Email: input.Email,
EncryptedPassword: input.EncryptedPassword,
DisplayName: input.DisplayName,
})
if err != nil {
var pgErr *pgconn.PgError
if errors.As(err, &pgErr) && pgErr.Code == "23505" {
return uuid.Nil, handlers.ErrUserAlreadyExists
}
return uuid.Nil, err
}
log.Info().
Str("component", "auth_store").
Str("action", "create_user").
Str("email", input.Email).
Msg("auth store mutated")
return createdID, nil
}
func (r *PostgresAuthRepository) GetAuthUserByEmail(ctx context.Context, email string) (handlers.AuthUser, error) {
row, err := r.queries.GetAuthUserByEmail(ctx, email)
if err != nil {
if errors.Is(err, pgx.ErrNoRows) {
return handlers.AuthUser{}, handlers.ErrUserNotFound
}
return handlers.AuthUser{}, err
}
return handlers.AuthUser{
ID: row.ID,
Email: row.Email,
EncryptedPassword: row.EncryptedPassword,
CreatedAt: row.CreatedAt.Time,
UpdatedAt: row.UpdatedAt.Time,
}, nil
}
func (r *PostgresAuthRepository) GetPublicUserByID(ctx context.Context, id uuid.UUID) (handlers.PublicUser, error) {
row, err := r.queries.GetPublicUserByID(ctx, id)
if err != nil {
if errors.Is(err, pgx.ErrNoRows) {
return handlers.PublicUser{}, handlers.ErrUserNotFound
}
return handlers.PublicUser{}, err
}
return handlers.PublicUser{
ID: row.ID,
Email: row.Email,
DisplayName: row.DisplayName,
CreatedAt: row.CreatedAt.Time,
UpdatedAt: row.UpdatedAt.Time,
}, nil
}
func (r *PostgresAuthRepository) CreateSession(ctx context.Context, token string, userID uuid.UUID, expiresAt time.Time) error {
err := r.queries.CreateSession(ctx, sqlcdb.CreateSessionParams{
ID: uuid.New(),
SessionToken: token,
UserID: userID,
ExpiresAt: pgtypeTimestamptz(expiresAt),
})
return err
}
func (r *PostgresAuthRepository) GetSessionByToken(ctx context.Context, token string) (handlers.Session, error) {
row, err := r.queries.GetSessionByToken(ctx, token)
if err != nil {
if errors.Is(err, pgx.ErrNoRows) {
return handlers.Session{}, handlers.ErrSessionNotFound
}
return handlers.Session{}, err
}
return handlers.Session{
Token: row.SessionToken,
UserID: row.UserID,
CreatedAt: row.CreatedAt.Time,
UpdatedAt: row.UpdatedAt.Time,
ExpiresAt: row.ExpiresAt.Time,
}, nil
}
func (r *PostgresAuthRepository) DeleteSessionByToken(ctx context.Context, token string) error {
rows, err := r.queries.DeleteSessionByToken(ctx, token)
if err != nil {
return err
}
if rows == 0 {
return handlers.ErrSessionNotFound
}
return nil
}
func (r *PostgresAuthRepository) CreateTablo(ctx context.Context, input tablomodel.CreateInput) (tablomodel.Record, error) {
row, err := r.queries.CreateTablo(ctx, sqlcdb.CreateTabloParams{
ID: uuid.New(),
OwnerID: input.OwnerID,
Name: strings.TrimSpace(input.Name),
Status: string(input.Status),
})
if err != nil {
return tablomodel.Record{}, err
}
return mapTabloRecord(row), nil
}
func (r *PostgresAuthRepository) ListTablos(ctx context.Context, input tablomodel.ListInput) ([]tablomodel.Record, error) {
params := sqlcdb.ListTablosParams{
OwnerID: input.OwnerID,
Query: nullableText(strings.TrimSpace(input.Query)),
Status: nullableStatus(input.Status),
}
rows, err := r.queries.ListTablos(ctx, params)
if err != nil {
return nil, err
}
tablos := make([]tablomodel.Record, 0, len(rows))
for _, row := range rows {
tablos = append(tablos, mapTabloRecord(row))
}
return tablos, nil
}
func (r *PostgresAuthRepository) SoftDeleteTablo(ctx context.Context, tabloID uuid.UUID, ownerID uuid.UUID) error {
rows, err := r.queries.SoftDeleteTablo(ctx, sqlcdb.SoftDeleteTabloParams{
ID: tabloID,
OwnerID: ownerID,
})
if err != nil {
return err
}
if rows == 0 {
return tablomodel.ErrNotFound
}
return nil
}
func pgtypeTimestamptz(value time.Time) pgtype.Timestamptz {
return pgtype.Timestamptz{Time: value, Valid: true}
}
func nullableText(value string) pgtype.Text {
if value == "" {
return pgtype.Text{}
}
return pgtype.Text{String: value, Valid: true}
}
func nullableStatus(value *tablomodel.Status) pgtype.Text {
if value == nil {
return pgtype.Text{}
}
return nullableText(string(*value))
}
func mapTabloRecord(row sqlcdb.Tablo) tablomodel.Record {
record := tablomodel.Record{
ID: row.ID,
OwnerID: row.OwnerID,
Name: row.Name,
Status: tablomodel.Status(row.Status),
CreatedAt: row.CreatedAt.Time,
UpdatedAt: row.UpdatedAt.Time,
}
if row.DeletedAt.Valid {
deletedAt := row.DeletedAt.Time
record.DeletedAt = &deletedAt
}
return record
}