refactor
All checks were successful
continuous-integration/drone/push Build is passing

This commit is contained in:
aovantsev
2025-10-10 19:56:06 +03:00
parent f081c9d947
commit 73ed514a34
30 changed files with 1728 additions and 1020 deletions

View File

@@ -0,0 +1,90 @@
package postgres
import (
"database/sql"
"fmt"
"counter/internal/infrastructure/config"
"counter/internal/infrastructure/logging"
_ "github.com/lib/pq"
)
// Connection manages the database connection
type Connection struct {
db *sql.DB
}
// NewConnection creates a new database connection
func NewConnection(cfg *config.Config, logger logging.Logger) (*Connection, error) {
db, err := sql.Open("postgres", cfg.DatabaseURL)
if err != nil {
return nil, fmt.Errorf("failed to open database: %w", err)
}
// Test the connection
if err = db.Ping(); err != nil {
return nil, fmt.Errorf("failed to ping database: %w", err)
}
logger.Info("✅ Database connection established successfully")
conn := &Connection{db: db}
// Create tables
if err := conn.CreateTables(); err != nil {
return nil, fmt.Errorf("failed to create tables: %w", err)
}
return conn, nil
}
// GetDB returns the database connection
func (c *Connection) GetDB() *sql.DB {
return c.db
}
// Close closes the database connection
func (c *Connection) Close() error {
return c.db.Close()
}
// CreateTables creates the necessary database tables
func (c *Connection) CreateTables() error {
queries := []string{
`CREATE TABLE IF NOT EXISTS users (
id SERIAL PRIMARY KEY,
username VARCHAR(50) UNIQUE NOT NULL,
email VARCHAR(255) UNIQUE NOT NULL,
password VARCHAR(255) NOT NULL,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
)`,
`CREATE TABLE IF NOT EXISTS counters (
id SERIAL PRIMARY KEY,
user_id INTEGER REFERENCES users(id) ON DELETE CASCADE,
name VARCHAR(100) NOT NULL,
description TEXT,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
)`,
`CREATE TABLE IF NOT EXISTS counter_entries (
id SERIAL PRIMARY KEY,
counter_id INTEGER REFERENCES counters(id) ON DELETE CASCADE,
value INTEGER NOT NULL,
date DATE NOT NULL,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
)`,
`CREATE INDEX IF NOT EXISTS idx_counters_user_id ON counters(user_id)`,
`CREATE INDEX IF NOT EXISTS idx_counter_entries_counter_id ON counter_entries(counter_id)`,
`CREATE INDEX IF NOT EXISTS idx_counter_entries_date ON counter_entries(date)`,
}
for _, query := range queries {
if _, err := c.db.Exec(query); err != nil {
return fmt.Errorf("failed to execute query: %w", err)
}
}
return nil
}

View File

@@ -0,0 +1,283 @@
package postgres
import (
"context"
"database/sql"
"fmt"
"time"
"counter/internal/domain/entities"
"counter/internal/domain/repositories"
)
// CounterRepository implements the CounterRepository interface for PostgreSQL
type CounterRepository struct {
db *sql.DB
}
// NewCounterRepository creates a new counter repository
func NewCounterRepository(db *sql.DB) repositories.CounterRepository {
return &CounterRepository{db: db}
}
// Create creates a new counter
func (r *CounterRepository) Create(ctx context.Context, counter *entities.Counter) error {
query := `
INSERT INTO counters (user_id, name, description)
VALUES ($1, $2, $3)
RETURNING id, created_at, updated_at
`
err := r.db.QueryRowContext(ctx, query, counter.UserID, counter.Name, counter.Description).
Scan(&counter.ID, &counter.CreatedAt, &counter.UpdatedAt)
if err != nil {
return err
}
return nil
}
// FindByID finds a counter by ID with stats
func (r *CounterRepository) FindByID(ctx context.Context, id, userID int) (*entities.CounterWithStats, error) {
query := `
SELECT c.id, c.user_id, c.name, c.description, c.created_at, c.updated_at,
COALESCE(SUM(ce.value), 0) as total_value,
COALESCE(SUM(CASE WHEN ce.date = CURRENT_DATE THEN ce.value ELSE 0 END), 0) as today_value,
COALESCE(SUM(CASE WHEN ce.date >= CURRENT_DATE - INTERVAL '7 days' THEN ce.value ELSE 0 END), 0) as week_value,
COALESCE(SUM(CASE WHEN ce.date >= DATE_TRUNC('month', CURRENT_DATE) THEN ce.value ELSE 0 END), 0) as month_value,
COUNT(ce.id) as entry_count
FROM counters c
LEFT JOIN counter_entries ce ON c.id = ce.counter_id
WHERE c.id = $1 AND c.user_id = $2
GROUP BY c.id, c.user_id, c.name, c.description, c.created_at, c.updated_at
`
counter := &entities.CounterWithStats{}
err := r.db.QueryRowContext(ctx, query, id, userID).
Scan(&counter.ID, &counter.UserID, &counter.Name, &counter.Description,
&counter.CreatedAt, &counter.UpdatedAt, &counter.TotalValue,
&counter.TodayValue, &counter.WeekValue, &counter.MonthValue, &counter.EntryCount)
if err == sql.ErrNoRows {
return nil, entities.ErrCounterNotFound
}
if err != nil {
return nil, err
}
return counter, nil
}
// FindByUserID finds all counters for a user with stats
func (r *CounterRepository) FindByUserID(ctx context.Context, userID int, search string) ([]*entities.CounterWithStats, error) {
query := `
SELECT c.id, c.user_id, c.name, c.description, c.created_at, c.updated_at,
COALESCE(SUM(ce.value), 0) as total_value,
COALESCE(SUM(CASE WHEN ce.date = CURRENT_DATE THEN ce.value ELSE 0 END), 0) as today_value,
COALESCE(SUM(CASE WHEN ce.date >= CURRENT_DATE - INTERVAL '7 days' THEN ce.value ELSE 0 END), 0) as week_value,
COALESCE(SUM(CASE WHEN ce.date >= DATE_TRUNC('month', CURRENT_DATE) THEN ce.value ELSE 0 END), 0) as month_value,
COUNT(ce.id) as entry_count
FROM counters c
LEFT JOIN counter_entries ce ON c.id = ce.counter_id
WHERE c.user_id = $1
`
args := []interface{}{userID}
if search != "" {
query += " AND (c.name ILIKE $2 OR c.description ILIKE $2)"
args = append(args, "%"+search+"%")
}
query += " GROUP BY c.id, c.user_id, c.name, c.description, c.created_at, c.updated_at ORDER BY c.updated_at DESC"
rows, err := r.db.QueryContext(ctx, query, args...)
if err != nil {
return nil, err
}
defer rows.Close()
var counters []*entities.CounterWithStats
for rows.Next() {
counter := &entities.CounterWithStats{}
err := rows.Scan(&counter.ID, &counter.UserID, &counter.Name, &counter.Description,
&counter.CreatedAt, &counter.UpdatedAt, &counter.TotalValue,
&counter.TodayValue, &counter.WeekValue, &counter.MonthValue, &counter.EntryCount)
if err != nil {
return nil, err
}
counters = append(counters, counter)
}
return counters, nil
}
// Update updates a counter
func (r *CounterRepository) Update(ctx context.Context, counter *entities.Counter) error {
query := `
UPDATE counters
SET name = $1, description = $2, updated_at = CURRENT_TIMESTAMP
WHERE id = $3 AND user_id = $4
RETURNING updated_at
`
err := r.db.QueryRowContext(ctx, query, counter.Name, counter.Description, counter.ID, counter.UserID).
Scan(&counter.UpdatedAt)
if err == sql.ErrNoRows {
return entities.ErrCounterNotFound
}
if err != nil {
return err
}
return nil
}
// Delete deletes a counter
func (r *CounterRepository) Delete(ctx context.Context, id, userID int) error {
query := `DELETE FROM counters WHERE id = $1 AND user_id = $2`
result, err := r.db.ExecContext(ctx, query, id, userID)
if err != nil {
return err
}
rowsAffected, err := result.RowsAffected()
if err != nil {
return err
}
if rowsAffected == 0 {
return entities.ErrCounterNotFound
}
return nil
}
// AddEntry adds a new counter entry
func (r *CounterRepository) AddEntry(ctx context.Context, entry *entities.CounterEntry) error {
query := `
INSERT INTO counter_entries (counter_id, value, date)
VALUES ($1, $2, $3)
RETURNING id, created_at
`
err := r.db.QueryRowContext(ctx, query, entry.CounterID, entry.Value, entry.Date).
Scan(&entry.ID, &entry.CreatedAt)
if err != nil {
return err
}
// Update counter's updated_at timestamp
_, err = r.db.ExecContext(ctx, "UPDATE counters SET updated_at = CURRENT_TIMESTAMP WHERE id = $1", entry.CounterID)
if err != nil {
// Log error but don't fail the request
// This could be improved with proper logging
}
return nil
}
// GetEntries retrieves entries for a specific counter
func (r *CounterRepository) GetEntries(ctx context.Context, counterID, userID int, startDate, endDate *time.Time) ([]*entities.CounterEntry, error) {
// First verify counter belongs to user
exists, err := r.Exists(ctx, counterID, userID)
if err != nil {
return nil, err
}
if !exists {
return nil, entities.ErrCounterNotFound
}
query := `
SELECT id, counter_id, value, date, created_at
FROM counter_entries
WHERE counter_id = $1
`
args := []interface{}{counterID}
if startDate != nil {
query += fmt.Sprintf(" AND date >= $%d", len(args)+1)
args = append(args, *startDate)
if endDate != nil {
query += fmt.Sprintf(" AND date <= $%d", len(args)+1)
args = append(args, *endDate)
}
} else if endDate != nil {
query += fmt.Sprintf(" AND date <= $%d", len(args)+1)
args = append(args, *endDate)
}
query += " ORDER BY date DESC, created_at DESC"
rows, err := r.db.QueryContext(ctx, query, args...)
if err != nil {
return nil, err
}
defer rows.Close()
var entries []*entities.CounterEntry
for rows.Next() {
entry := &entities.CounterEntry{}
err := rows.Scan(&entry.ID, &entry.CounterID, &entry.Value, &entry.Date, &entry.CreatedAt)
if err != nil {
return nil, err
}
entries = append(entries, entry)
}
return entries, nil
}
// GetDailyStats retrieves daily statistics for a counter
func (r *CounterRepository) GetDailyStats(ctx context.Context, counterID, userID int, days int) ([]*entities.DailyStat, error) {
// First verify counter belongs to user
exists, err := r.Exists(ctx, counterID, userID)
if err != nil {
return nil, err
}
if !exists {
return nil, entities.ErrCounterNotFound
}
query := `
SELECT date, SUM(value) as daily_total
FROM counter_entries
WHERE counter_id = $1 AND date >= CURRENT_DATE - INTERVAL '%d days'
GROUP BY date
ORDER BY date DESC
`
rows, err := r.db.QueryContext(ctx, fmt.Sprintf(query, days), counterID)
if err != nil {
return nil, err
}
defer rows.Close()
var stats []*entities.DailyStat
for rows.Next() {
stat := &entities.DailyStat{}
err := rows.Scan(&stat.Date, &stat.Total)
if err != nil {
return nil, err
}
stats = append(stats, stat)
}
return stats, nil
}
// Exists checks if a counter exists and belongs to the user
func (r *CounterRepository) Exists(ctx context.Context, id, userID int) (bool, error) {
query := `SELECT EXISTS(SELECT 1 FROM counters WHERE id = $1 AND user_id = $2)`
var exists bool
err := r.db.QueryRowContext(ctx, query, id, userID).Scan(&exists)
if err != nil {
return false, err
}
return exists, nil
}

View File

@@ -0,0 +1,146 @@
package postgres
import (
"context"
"database/sql"
"counter/internal/domain/entities"
"counter/internal/domain/repositories"
)
// UserRepository implements the UserRepository interface for PostgreSQL
type UserRepository struct {
db *sql.DB
}
// NewUserRepository creates a new user repository
func NewUserRepository(db *sql.DB) repositories.UserRepository {
return &UserRepository{db: db}
}
// Create creates a new user
func (r *UserRepository) Create(ctx context.Context, user *entities.User) error {
query := `
INSERT INTO users (username, email, password)
VALUES ($1, $2, $3)
RETURNING id, created_at, updated_at
`
err := r.db.QueryRowContext(ctx, query, user.Username, user.Email, user.Password).
Scan(&user.ID, &user.CreatedAt, &user.UpdatedAt)
if err != nil {
return err
}
return nil
}
// FindByID finds a user by ID
func (r *UserRepository) FindByID(ctx context.Context, id int) (*entities.User, error) {
query := `
SELECT id, username, email, password, created_at, updated_at
FROM users
WHERE id = $1
`
user := &entities.User{}
err := r.db.QueryRowContext(ctx, query, id).
Scan(&user.ID, &user.Username, &user.Email, &user.Password, &user.CreatedAt, &user.UpdatedAt)
if err == sql.ErrNoRows {
return nil, entities.ErrUserNotFound
}
if err != nil {
return nil, err
}
return user, nil
}
// FindByUsername finds a user by username
func (r *UserRepository) FindByUsername(ctx context.Context, username string) (*entities.User, error) {
query := `
SELECT id, username, email, password, created_at, updated_at
FROM users
WHERE username = $1
`
user := &entities.User{}
err := r.db.QueryRowContext(ctx, query, username).
Scan(&user.ID, &user.Username, &user.Email, &user.Password, &user.CreatedAt, &user.UpdatedAt)
if err == sql.ErrNoRows {
return nil, entities.ErrUserNotFound
}
if err != nil {
return nil, err
}
return user, nil
}
// FindByEmail finds a user by email
func (r *UserRepository) FindByEmail(ctx context.Context, email string) (*entities.User, error) {
query := `
SELECT id, username, email, password, created_at, updated_at
FROM users
WHERE email = $1
`
user := &entities.User{}
err := r.db.QueryRowContext(ctx, query, email).
Scan(&user.ID, &user.Username, &user.Email, &user.Password, &user.CreatedAt, &user.UpdatedAt)
if err == sql.ErrNoRows {
return nil, entities.ErrUserNotFound
}
if err != nil {
return nil, err
}
return user, nil
}
// Update updates a user
func (r *UserRepository) Update(ctx context.Context, user *entities.User) error {
query := `
UPDATE users
SET username = $1, email = $2, password = $3, updated_at = CURRENT_TIMESTAMP
WHERE id = $4
RETURNING updated_at
`
err := r.db.QueryRowContext(ctx, query, user.Username, user.Email, user.Password, user.ID).
Scan(&user.UpdatedAt)
if err == sql.ErrNoRows {
return entities.ErrUserNotFound
}
if err != nil {
return err
}
return nil
}
// Delete deletes a user
func (r *UserRepository) Delete(ctx context.Context, id int) error {
query := `DELETE FROM users WHERE id = $1`
result, err := r.db.ExecContext(ctx, query, id)
if err != nil {
return err
}
rowsAffected, err := result.RowsAffected()
if err != nil {
return err
}
if rowsAffected == 0 {
return entities.ErrUserNotFound
}
return nil
}