This commit is contained in:
2025-11-03 12:24:01 +02:00
commit 0806865287
177 changed files with 18453 additions and 0 deletions

View File

@@ -0,0 +1,22 @@
package repository
import (
"context"
"go-server/internal/models"
"gorm.io/gorm"
)
type CategoryRepository struct {
db *gorm.DB
}
func NewCategoryRepository(db *gorm.DB) *CategoryRepository {
return &CategoryRepository{db: db}
}
func (r *CategoryRepository) GetAll(ctx context.Context) ([]models.Category, error) {
var categories []models.Category
err := r.db.WithContext(ctx).Find(&categories).Error
return categories, err
}

View File

@@ -0,0 +1,187 @@
package repository
import (
"context"
"go-server/internal/models"
"gorm.io/gorm"
)
type DailyOverviewRepository struct {
db *gorm.DB
}
func NewDailyOverviewRepository(db *gorm.DB) *DailyOverviewRepository {
return &DailyOverviewRepository{db: db}
}
// NutrientTotal represents the aggregated total for a nutrient across all supplements
type NutrientTotal struct {
NutrientID models.ULID `json:"nutrientId" db:"nutrient_id"`
NutrientName string `json:"nutrientName" db:"nutrient_name"`
Description string `json:"description" db:"description"`
TotalAmount string `json:"totalAmount" db:"total_amount"`
Unit string `json:"unit" db:"unit"`
Categories []string `json:"categories" db:"categories"`
}
// SupplementNutrientDetails represents detailed breakdown by supplement
type SupplementNutrientDetails struct {
SupplementID models.ULID `json:"supplementId" db:"supplement_id"`
SupplementName string `json:"supplementName" db:"supplement_name"`
NutrientID models.ULID `json:"nutrientId" db:"nutrient_id"`
NutrientName string `json:"nutrientName" db:"nutrient_name"`
Amount string `json:"amount" db:"amount"`
Unit string `json:"unit" db:"unit"`
ServingSize string `json:"servingSize" db:"serving_size"`
ReferenceIntake string `json:"referenceIntake" db:"reference_intake"`
}
// GetNutrientTotals returns aggregated nutrient totals across all supplements
// This is perfect for a daily overview where you want to see total intake
func (r *DailyOverviewRepository) GetNutrientTotals(ctx context.Context) ([]NutrientTotal, error) {
var results []NutrientTotal
query := `
SELECT
n.id as nutrient_id,
n.name as nutrient_name,
n.description,
sn.per_serving_reference_intake,
-- For now, we'll concatenate amounts (later we can parse and sum numeric values)
STRING_AGG(sn.per_serving, ' + ') as total_amount,
-- Extract unit from first entry (assumption: same nutrient has same unit)
SPLIT_PART(MIN(sn.per_serving), ' ', 2) as unit,
-- Get all categories for this nutrient
ARRAY_AGG(DISTINCT c.name) as categories
FROM nutrients n
LEFT JOIN supplement_nutrients sn ON n.id = sn.nutrient_id
LEFT JOIN supplements s ON sn.supplement_id = s.id
LEFT JOIN nutrient_categories nc ON n.id = nc.nutrient_id
LEFT JOIN categories c ON nc.category_id = c.id
WHERE sn.id IS NOT NULL -- Only nutrients that are in supplements
GROUP BY n.id, n.name, n.description, sn.per_serving_reference_intake
ORDER BY n.name
`
err := r.db.WithContext(ctx).Raw(query).Scan(&results).Error
return results, err
}
// GetSupplementBreakdown returns detailed breakdown of nutrients by supplement
func (r *DailyOverviewRepository) GetSupplementBreakdown(ctx context.Context) ([]SupplementNutrientDetails, error) {
var results []SupplementNutrientDetails
query := `
SELECT
s.id as supplement_id,
s.name as supplement_name,
n.id as nutrient_id,
n.name as nutrient_name,
sn.per_serving as amount,
SPLIT_PART(sn.per_serving, ' ', 2) as unit,
sn.serving_size,
sn.per_serving_reference_intake as reference_intake
FROM supplements s
JOIN supplement_nutrients sn ON s.id = sn.supplement_id
JOIN nutrients n ON sn.nutrient_id = n.id
ORDER BY s.name, n.name
`
err := r.db.WithContext(ctx).Raw(query).Scan(&results).Error
return results, err
}
// GetNutrientsByCategory returns nutrients grouped by category with totals
func (r *DailyOverviewRepository) GetNutrientsByCategory(ctx context.Context) (map[string][]NutrientTotal, error) {
var results []struct {
CategoryName string `db:"category_name"`
CategoryID models.ULID `db:"category_id"`
NutrientID models.ULID `db:"nutrient_id"`
NutrientName string `db:"nutrient_name"`
Description string `db:"description"`
TotalAmount string `db:"total_amount"`
Unit string `db:"unit"`
}
query := `
SELECT
c.name as category_name,
c.id as category_id,
n.id as nutrient_id,
n.name as nutrient_name,
n.description,
STRING_AGG(sn.per_serving, ' + ') as total_amount,
SPLIT_PART(MIN(sn.per_serving), ' ', 2) as unit
FROM categories c
JOIN nutrient_categories nc ON c.id = nc.category_id
JOIN nutrients n ON nc.nutrient_id = n.id
LEFT JOIN supplement_nutrients sn ON n.id = sn.nutrient_id
WHERE sn.id IS NOT NULL
GROUP BY c.name, n.id, n.name, n.description, c.id
ORDER BY c.name, n.name
`
err := r.db.WithContext(ctx).Raw(query).Scan(&results).Error
if err != nil {
return nil, err
}
// Group by category
categoryMap := make(map[string][]NutrientTotal)
for _, result := range results {
nutrient := NutrientTotal{
NutrientID: result.NutrientID,
NutrientName: result.NutrientName,
Description: result.Description,
TotalAmount: result.TotalAmount,
Unit: result.Unit,
Categories: []string{result.CategoryID.String()},
}
categoryMap[result.CategoryName] = append(categoryMap[result.CategoryName], nutrient)
}
return categoryMap, nil
}
// ExecuteRawQuery allows executing arbitrary SQL queries and scanning into any struct
// This gives you full flexibility for custom queries
func (r *DailyOverviewRepository) ExecuteRawQuery(ctx context.Context, query string, dest interface{}, args ...interface{}) error {
return r.db.WithContext(ctx).Raw(query, args...).Scan(dest).Error
}
// ExecuteRawQueryWithResult executes a raw query and returns the result as a map
// Useful for dynamic queries where you don't know the structure ahead of time
func (r *DailyOverviewRepository) ExecuteRawQueryWithResult(ctx context.Context, query string, args ...interface{}) ([]map[string]interface{}, error) {
rows, err := r.db.WithContext(ctx).Raw(query, args...).Rows()
if err != nil {
return nil, err
}
defer rows.Close()
columns, err := rows.Columns()
if err != nil {
return nil, err
}
var results []map[string]interface{}
for rows.Next() {
values := make([]interface{}, len(columns))
valuePtrs := make([]interface{}, len(columns))
for i := range columns {
valuePtrs[i] = &values[i]
}
if err := rows.Scan(valuePtrs...); err != nil {
return nil, err
}
result := make(map[string]interface{})
for i, col := range columns {
result[col] = values[i]
}
results = append(results, result)
}
return results, nil
}

View File

@@ -0,0 +1,28 @@
package repository
import (
"context"
"go-server/internal/models"
"gorm.io/gorm"
)
type NutrientRepository struct {
db *gorm.DB
}
func NewNutrientRepository(db *gorm.DB) *NutrientRepository {
return &NutrientRepository{db: db}
}
func (r *NutrientRepository) GetAll(ctx context.Context) ([]*models.Nutrient, error) {
var nutrients []*models.Nutrient
err := r.db.WithContext(ctx).
Preload("Categories").
Find(&nutrients).
Error
if err != nil {
return nil, err
}
return nutrients, nil
}

View File

@@ -0,0 +1,57 @@
package repository
import (
"context"
"go-server/internal/models"
"gorm.io/gorm"
)
type SupplementRepository struct {
db *gorm.DB
}
func NewSupplementRepository(db *gorm.DB) *SupplementRepository {
return &SupplementRepository{db: db}
}
func (r *SupplementRepository) GetAll(ctx context.Context) ([]*models.Supplement, error) {
var supplements []*models.Supplement
err := r.db.WithContext(ctx).
Preload("Nutrients").
Preload("SupplementNutrients").
Find(&supplements).
Error
if err != nil {
return nil, err
}
return supplements, nil
}
func (r *SupplementRepository) GetById(ctx context.Context, id string) (*models.Supplement, error) {
var supplement *models.Supplement
err := r.db.WithContext(ctx).
Preload("Nutrients").
Preload("SupplementNutrients").
First(&supplement, "id = ?", id).
Error
if err != nil {
return nil, err
}
return supplement, nil
}
func (r *SupplementRepository) GetDailySupplementsOverview(ctx context.Context) ([]*models.SupplementNutrientOverview, error) {
var supplementNutrientOverview []*models.SupplementNutrientOverview
err := r.db.WithContext(ctx).
Table("supplements").
Select("supplements.id as supplement_id, supplements.name as supplement_name, supplements.description as supplement_description, supplement_nutrients.serving_size as serving_size, supplement_nutrients.per_serving as per_serving, supplement_nutrients.per_serving_reference_intake as per_serving_reference_intake, nutrients.id as nutrient_id, nutrients.name as nutrient_name, nutrients.description as nutrient_description").
Joins("INNER JOIN supplement_nutrients ON supplements.id = supplement_nutrients.supplement_id").
Joins("JOIN nutrients ON supplement_nutrients.nutrient_id = nutrients.id").
Find(&supplementNutrientOverview).
Error
if err != nil {
return nil, err
}
return supplementNutrientOverview, nil
}

View File

@@ -0,0 +1,247 @@
package repository
import (
"context"
"go-server/internal/models"
"time"
"gorm.io/gorm"
)
type TodoRepository struct {
db *gorm.DB
}
func NewTodoRepository(db *gorm.DB) *TodoRepository {
return &TodoRepository{db: db}
}
// CreateTodo creates a new todo for a user
func (r *TodoRepository) CreateTodo(ctx context.Context, todo *models.Todo) error {
return r.db.WithContext(ctx).Create(todo).Error
}
// GetTodosByUserID gets all active todos for a user
func (r *TodoRepository) GetTodosByUserID(ctx context.Context, userID models.ULID) ([]models.Todo, error) {
var todos []models.Todo
err := r.db.WithContext(ctx).
Where("user_id = ? AND is_active = ?", userID, true).
Order("created_at ASC").
Find(&todos).Error
return todos, err
}
// GetTodoByID gets a todo by ID and user ID (for security)
func (r *TodoRepository) GetTodoByID(ctx context.Context, todoID, userID models.ULID) (*models.Todo, error) {
var todo models.Todo
err := r.db.WithContext(ctx).
Where("id = ? AND user_id = ?", todoID, userID).
First(&todo).Error
if err != nil {
return nil, err
}
return &todo, nil
}
// UpdateTodo updates a todo
func (r *TodoRepository) UpdateTodo(ctx context.Context, todo *models.Todo) error {
return r.db.WithContext(ctx).Save(todo).Error
}
// DeleteTodo soft deletes a todo (sets is_active to false)
func (r *TodoRepository) DeleteTodo(ctx context.Context, todoID, userID models.ULID) error {
return r.db.WithContext(ctx).
Model(&models.Todo{}).
Where("id = ? AND user_id = ?", todoID, userID).
Update("is_active", false).Error
}
// CompleteTodo creates a completion record for a todo
func (r *TodoRepository) CompleteTodo(ctx context.Context, completion *models.TodoCompletion) error {
return r.db.WithContext(ctx).Create(completion).Error
}
// GetTodayCompletions gets all completions for today for a user
func (r *TodoRepository) GetTodayCompletions(ctx context.Context, userID models.ULID) ([]models.TodoCompletion, error) {
now := time.Now()
startOfDay := time.Date(now.Year(), now.Month(), now.Day(), 0, 0, 0, 0, now.Location())
endOfDay := startOfDay.Add(24 * time.Hour)
var completions []models.TodoCompletion
err := r.db.WithContext(ctx).
Preload("Todo").
Where("user_id = ? AND completed_at >= ? AND completed_at < ?", userID, startOfDay, endOfDay).
Order("completed_at DESC").
Find(&completions).Error
return completions, err
}
// GetCompletionsByDateRange gets completions for a date range
func (r *TodoRepository) GetCompletionsByDateRange(ctx context.Context, userID models.ULID, startDate, endDate time.Time) ([]models.TodoCompletion, error) {
var completions []models.TodoCompletion
err := r.db.WithContext(ctx).
Preload("Todo").
Where("user_id = ? AND completed_at >= ? AND completed_at < ?", userID, startDate, endDate).
Order("completed_at DESC").
Find(&completions).Error
return completions, err
}
// GetTodoWithStats gets todos with completion statistics using raw SQL for better performance
func (r *TodoRepository) GetTodosWithStats(ctx context.Context, userID models.ULID) ([]models.TodoWithStats, error) {
var results []models.TodoWithStats
query := `
WITH todo_stats AS (
SELECT
t.id,
t.user_id,
t.title,
t.description,
t.color,
t.is_active,
t.created_at,
t.updated_at,
COUNT(tc.id) as total_completions,
MAX(tc.completed_at) as last_completed_at,
CASE
WHEN MAX(tc.completed_at) >= CURRENT_DATE
THEN true
ELSE false
END as completed_today
FROM todos t
LEFT JOIN todo_completions tc ON t.id = tc.todo_id
WHERE t.user_id = ? AND t.is_active = true
GROUP BY t.id, t.user_id, t.title, t.description, t.color, t.is_active, t.created_at, t.updated_at
),
streak_calc AS (
SELECT
ts.*,
COALESCE(
(SELECT COUNT(*)
FROM generate_series(
CURRENT_DATE - INTERVAL '365 days',
CURRENT_DATE,
INTERVAL '1 day'
) AS date_series(date)
WHERE EXISTS (
SELECT 1 FROM todo_completions tc2
WHERE tc2.todo_id = ts.id
AND DATE(tc2.completed_at) = date_series.date
)
AND date_series.date <= CURRENT_DATE
AND NOT EXISTS (
SELECT 1 FROM generate_series(
date_series.date + INTERVAL '1 day',
CURRENT_DATE,
INTERVAL '1 day'
) AS gap_check(gap_date)
WHERE NOT EXISTS (
SELECT 1 FROM todo_completions tc3
WHERE tc3.todo_id = ts.id
AND DATE(tc3.completed_at) = gap_check.gap_date
)
)
), 0
) as current_streak
FROM todo_stats ts
)
SELECT
sc.*,
COALESCE(
(SELECT MAX(streak_length)
FROM (
SELECT COUNT(*) as streak_length
FROM (
SELECT
DATE(tc.completed_at) as completion_date,
ROW_NUMBER() OVER (ORDER BY DATE(tc.completed_at)) as rn,
DATE(tc.completed_at) - INTERVAL '1 day' * ROW_NUMBER() OVER (ORDER BY DATE(tc.completed_at)) as streak_group
FROM todo_completions tc
WHERE tc.todo_id = sc.id
) grouped
GROUP BY streak_group
) streaks), 0
) as longest_streak
FROM streak_calc sc
ORDER BY sc.created_at ASC
`
rows, err := r.db.WithContext(ctx).Raw(query, userID).Rows()
if err != nil {
return nil, err
}
defer rows.Close()
for rows.Next() {
var result models.TodoWithStats
var lastCompletedAt *time.Time
err := rows.Scan(
&result.ID,
&result.UserID,
&result.Title,
&result.Description,
&result.Color,
&result.IsActive,
&result.CreatedAt,
&result.UpdatedAt,
&result.TotalCompletions,
&lastCompletedAt,
&result.CompletedToday,
&result.CurrentStreak,
&result.LongestStreak,
)
if err != nil {
return nil, err
}
result.LastCompletedAt = lastCompletedAt
results = append(results, result)
}
return results, nil
}
// CheckTodoCompletedToday checks if a todo was completed today
func (r *TodoRepository) CheckTodoCompletedToday(ctx context.Context, todoID, userID models.ULID) (bool, error) {
now := time.Now()
startOfDay := time.Date(now.Year(), now.Month(), now.Day(), 0, 0, 0, 0, now.Location())
endOfDay := startOfDay.Add(24 * time.Hour)
var count int64
err := r.db.WithContext(ctx).
Model(&models.TodoCompletion{}).
Where("todo_id = ? AND user_id = ? AND completed_at >= ? AND completed_at < ?",
todoID, userID, startOfDay, endOfDay).
Count(&count).Error
return count > 0, err
}
// GetActivityLog gets activity log with pagination
func (r *TodoRepository) GetActivityLog(ctx context.Context, userID models.ULID, limit, offset int) ([]models.TodoCompletion, error) {
var completions []models.TodoCompletion
err := r.db.WithContext(ctx).
Preload("Todo").
Where("user_id = ?", userID).
Order("completed_at DESC").
Limit(limit).
Offset(offset).
Find(&completions).Error
return completions, err
}
// GetActivityLogByDate gets activity log for a specific date
func (r *TodoRepository) GetActivityLogByDate(ctx context.Context, userID models.ULID, date time.Time) ([]models.TodoCompletion, error) {
startOfDay := time.Date(date.Year(), date.Month(), date.Day(), 0, 0, 0, 0, date.Location())
endOfDay := startOfDay.Add(24 * time.Hour)
var completions []models.TodoCompletion
err := r.db.WithContext(ctx).
Preload("Todo").
Where("user_id = ? AND completed_at >= ? AND completed_at < ?", userID, startOfDay, endOfDay).
Order("completed_at DESC").
Find(&completions).Error
return completions, err
}

View File

@@ -0,0 +1,51 @@
package repository
import (
"context"
"errors"
"go-server/internal/models"
"github.com/google/uuid"
"gorm.io/gorm"
)
type UserRepository struct {
db *gorm.DB
}
func NewUserRepository(db *gorm.DB) *UserRepository {
return &UserRepository{db: db}
}
func (r *UserRepository) Create(ctx context.Context, user *models.User) error {
return r.db.WithContext(ctx).Create(user).Error
}
func (r *UserRepository) GetByID(ctx context.Context, id uuid.UUID) (*models.User, error) {
var user models.User
if err := r.db.WithContext(ctx).First(&user, id).Error; err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return nil, nil
}
return nil, err
}
return &user, nil
}
func (r *UserRepository) Update(ctx context.Context, user *models.User) error {
return r.db.WithContext(ctx).Save(user).Error
}
func (r *UserRepository) Delete(ctx context.Context, id uuid.UUID) error {
return r.db.WithContext(ctx).Delete(&models.User{}, id).Error
}
func (r *UserRepository) GetAll(ctx context.Context) ([]*models.User, error) {
var users []*models.User
err := r.db.WithContext(ctx).Find(&users).Error
if err != nil {
return nil, err
}
return users, nil
}