Initial commit: Homelab API

This commit is contained in:
Cosmo
2026-02-06 11:19:55 +00:00
commit 5a40127edd
26 changed files with 2807 additions and 0 deletions

View File

@@ -0,0 +1,271 @@
package repository
import (
"database/sql"
"errors"
"time"
"github.com/daniil/homelab-api/internal/model"
"github.com/jmoiron/sqlx"
"github.com/lib/pq"
)
var ErrHabitNotFound = errors.New("habit not found")
var ErrLogNotFound = errors.New("log not found")
type HabitRepository struct {
db *sqlx.DB
}
func NewHabitRepository(db *sqlx.DB) *HabitRepository {
return &HabitRepository{db: db}
}
func (r *HabitRepository) Create(habit *model.Habit) error {
query := `
INSERT INTO habits (user_id, name, description, color, icon, frequency, target_days, target_count)
VALUES ($1, $2, $3, $4, $5, $6, $7, $8)
RETURNING id, created_at, updated_at`
targetDays := pq.Array(habit.TargetDays)
if len(habit.TargetDays) == 0 {
targetDays = pq.Array([]int{0, 1, 2, 3, 4, 5, 6})
}
return r.db.QueryRow(query,
habit.UserID,
habit.Name,
habit.Description,
habit.Color,
habit.Icon,
habit.Frequency,
targetDays,
habit.TargetCount,
).Scan(&habit.ID, &habit.CreatedAt, &habit.UpdatedAt)
}
func (r *HabitRepository) GetByID(id, userID int64) (*model.Habit, error) {
var habit model.Habit
var targetDays pq.Int64Array
query := `
SELECT id, user_id, name, description, color, icon, frequency, target_days, target_count, is_archived, created_at, updated_at
FROM habits WHERE id = $1 AND user_id = $2`
err := r.db.QueryRow(query, id, userID).Scan(
&habit.ID, &habit.UserID, &habit.Name, &habit.Description,
&habit.Color, &habit.Icon, &habit.Frequency, &targetDays,
&habit.TargetCount, &habit.IsArchived, &habit.CreatedAt, &habit.UpdatedAt,
)
if err != nil {
if errors.Is(err, sql.ErrNoRows) {
return nil, ErrHabitNotFound
}
return nil, err
}
habit.TargetDays = make([]int, len(targetDays))
for i, v := range targetDays {
habit.TargetDays[i] = int(v)
}
return &habit, nil
}
func (r *HabitRepository) ListByUser(userID int64, includeArchived bool) ([]model.Habit, error) {
query := `
SELECT id, user_id, name, description, color, icon, frequency, target_days, target_count, is_archived, created_at, updated_at
FROM habits WHERE user_id = $1`
if !includeArchived {
query += " AND is_archived = FALSE"
}
query += " ORDER BY created_at DESC"
rows, err := r.db.Query(query, userID)
if err != nil {
return nil, err
}
defer rows.Close()
var habits []model.Habit
for rows.Next() {
var habit model.Habit
var targetDays pq.Int64Array
if err := rows.Scan(
&habit.ID, &habit.UserID, &habit.Name, &habit.Description,
&habit.Color, &habit.Icon, &habit.Frequency, &targetDays,
&habit.TargetCount, &habit.IsArchived, &habit.CreatedAt, &habit.UpdatedAt,
); err != nil {
return nil, err
}
habit.TargetDays = make([]int, len(targetDays))
for i, v := range targetDays {
habit.TargetDays[i] = int(v)
}
habits = append(habits, habit)
}
return habits, nil
}
func (r *HabitRepository) Update(habit *model.Habit) error {
query := `
UPDATE habits
SET name = $2, description = $3, color = $4, icon = $5, frequency = $6,
target_days = $7, target_count = $8, is_archived = $9, updated_at = CURRENT_TIMESTAMP
WHERE id = $1 AND user_id = $10
RETURNING updated_at`
return r.db.QueryRow(query,
habit.ID,
habit.Name,
habit.Description,
habit.Color,
habit.Icon,
habit.Frequency,
pq.Array(habit.TargetDays),
habit.TargetCount,
habit.IsArchived,
habit.UserID,
).Scan(&habit.UpdatedAt)
}
func (r *HabitRepository) Delete(id, userID int64) error {
query := `DELETE FROM habits WHERE id = $1 AND user_id = $2`
result, err := r.db.Exec(query, id, userID)
if err != nil {
return err
}
rows, _ := result.RowsAffected()
if rows == 0 {
return ErrHabitNotFound
}
return nil
}
// Logs
func (r *HabitRepository) CreateLog(log *model.HabitLog) error {
query := `
INSERT INTO habit_logs (habit_id, user_id, date, count, note)
VALUES ($1, $2, $3, $4, $5)
ON CONFLICT (habit_id, date)
DO UPDATE SET count = habit_logs.count + EXCLUDED.count, note = EXCLUDED.note
RETURNING id, created_at`
return r.db.QueryRow(query, log.HabitID, log.UserID, log.Date, log.Count, log.Note).
Scan(&log.ID, &log.CreatedAt)
}
func (r *HabitRepository) GetLogs(habitID, userID int64, from, to time.Time) ([]model.HabitLog, error) {
query := `
SELECT id, habit_id, user_id, date, count, note, created_at
FROM habit_logs
WHERE habit_id = $1 AND user_id = $2 AND date >= $3 AND date <= $4
ORDER BY date DESC`
var logs []model.HabitLog
if err := r.db.Select(&logs, query, habitID, userID, from, to); err != nil {
return nil, err
}
return logs, nil
}
func (r *HabitRepository) DeleteLog(logID, userID int64) error {
query := `DELETE FROM habit_logs WHERE id = $1 AND user_id = $2`
result, err := r.db.Exec(query, logID, userID)
if err != nil {
return err
}
rows, _ := result.RowsAffected()
if rows == 0 {
return ErrLogNotFound
}
return nil
}
func (r *HabitRepository) GetUserLogsForDate(userID int64, date time.Time) ([]model.HabitLog, error) {
query := `
SELECT id, habit_id, user_id, date, count, note, created_at
FROM habit_logs
WHERE user_id = $1 AND date = $2`
var logs []model.HabitLog
if err := r.db.Select(&logs, query, userID, date); err != nil {
return nil, err
}
return logs, nil
}
func (r *HabitRepository) GetStats(habitID, userID int64) (*model.HabitStats, error) {
stats := &model.HabitStats{HabitID: habitID}
// Total logs
r.db.Get(&stats.TotalLogs, `SELECT COUNT(*) FROM habit_logs WHERE habit_id = $1 AND user_id = $2`, habitID, userID)
// This week
weekStart := time.Now().AddDate(0, 0, -int(time.Now().Weekday()))
r.db.Get(&stats.ThisWeek, `SELECT COUNT(*) FROM habit_logs WHERE habit_id = $1 AND user_id = $2 AND date >= $3`, habitID, userID, weekStart)
// This month
monthStart := time.Date(time.Now().Year(), time.Now().Month(), 1, 0, 0, 0, 0, time.UTC)
r.db.Get(&stats.ThisMonth, `SELECT COUNT(*) FROM habit_logs WHERE habit_id = $1 AND user_id = $2 AND date >= $3`, habitID, userID, monthStart)
// Streaks calculation
stats.CurrentStreak, stats.LongestStreak = r.calculateStreaks(habitID, userID)
return stats, nil
}
func (r *HabitRepository) calculateStreaks(habitID, userID int64) (current, longest int) {
query := `SELECT date FROM habit_logs WHERE habit_id = $1 AND user_id = $2 ORDER BY date DESC`
var dates []time.Time
if err := r.db.Select(&dates, query, habitID, userID); err != nil || len(dates) == 0 {
return 0, 0
}
today := time.Now().Truncate(24 * time.Hour)
yesterday := today.AddDate(0, 0, -1)
// Current streak
if dates[0].Truncate(24*time.Hour).Equal(today) || dates[0].Truncate(24*time.Hour).Equal(yesterday) {
current = 1
for i := 1; i < len(dates); i++ {
expected := dates[i-1].AddDate(0, 0, -1).Truncate(24 * time.Hour)
if dates[i].Truncate(24 * time.Hour).Equal(expected) {
current++
} else {
break
}
}
}
// Longest streak
streak := 1
longest = 1
for i := 1; i < len(dates); i++ {
expected := dates[i-1].AddDate(0, 0, -1).Truncate(24 * time.Hour)
if dates[i].Truncate(24 * time.Hour).Equal(expected) {
streak++
if streak > longest {
longest = streak
}
} else {
streak = 1
}
}
return current, longest
}