src/database/models.go (view raw)
1package database
2
3import (
4 "log"
5 "os"
6 "path/filepath"
7 "time"
8
9 "github.com/glebarez/sqlite"
10 "gorm.io/gorm"
11 "gorm.io/gorm/logger"
12)
13
14type Database struct {
15 *gorm.DB
16}
17
18type User struct {
19 ID uint `gorm:"primaryKey" json:"id"`
20 Name string `gorm:"size:50" json:"name"`
21 IsFemale bool `json:"isFemale"`
22 Height *float64 `json:"height"` // In cm
23 Weight *float64 `json:"weight"` // In kg
24 BirthDate *time.Time `json:"birthDate"`
25 CreatedAt time.Time `json:"createdAt"`
26 UpdatedAt time.Time `json:"updatedAt"`
27 DeletedAt gorm.DeletedAt `gorm:"index" json:"deletedAt"`
28}
29
30type Exercise struct {
31 ID uint `gorm:"primaryKey;autoIncrement" json:"id"`
32 Name string `gorm:"not null;uniqueIndex" json:"name"`
33 Level string `gorm:"size:50;not null" json:"level"`
34 Category string `gorm:"size:50;not null" json:"category"`
35 Force *string `gorm:"size:50" json:"force"`
36 Mechanic *string `gorm:"size:50" json:"mechanic"`
37 Equipment *string `gorm:"size:50" json:"equipment"`
38 Instructions *string `json:"instructions"`
39
40 PrimaryMuscles []Muscle `gorm:"many2many:exercise_primary_muscles;constraint:OnDelete:CASCADE" json:"primaryMuscles"`
41 SecondaryMuscles []Muscle `gorm:"many2many:exercise_secondary_muscles;constraint:OnDelete:CASCADE" json:"secondaryMuscles"`
42
43 CreatedAt time.Time `json:"createdAt"`
44 UpdatedAt time.Time `json:"updatedAt"`
45}
46
47type Muscle struct {
48 ID uint `gorm:"primaryKey" json:"id"`
49 Name string `gorm:"uniqueIndex;size:50;not null" json:"name"`
50
51 CreatedAt time.Time `json:"createdAt"`
52 UpdatedAt time.Time `json:"updatedAt"`
53}
54
55// Routine represents a workout routine blueprint
56type Routine struct {
57 ID uint `gorm:"primaryKey" json:"id"`
58 Name string `gorm:"size:100;not null" json:"name"`
59 Description string `gorm:"size:500" json:"description"`
60 CreatedAt time.Time `json:"createdAt"`
61 UpdatedAt time.Time `json:"updatedAt"`
62
63 Items []RoutineItem `json:"items"`
64}
65
66// RoutineItem can be either a single exercise or a superset
67type RoutineItem struct {
68 ID uint `gorm:"primaryKey" json:"id"`
69 RoutineID uint `gorm:"index;not null" json:"routineId"`
70 Type string `gorm:"size:20;not null" json:"type"` // "exercise" or "superset"
71 RestTime int `gorm:"default:0" json:"restTime"` // In seconds
72 OrderIndex int `gorm:"not null" json:"orderIndex"`
73 CreatedAt time.Time `json:"createdAt"`
74 UpdatedAt time.Time `json:"updatedAt"`
75
76 Routine Routine `json:"-"`
77 ExerciseItems []ExerciseItem `json:"exerciseItems,omitempty"` // For both single exercises and superset items
78}
79
80// ExerciseItem represents an exercise within a routine item (could be standalone or part of superset)
81type ExerciseItem struct {
82 ID uint `gorm:"primaryKey" json:"id"`
83 RoutineItemID uint `gorm:"index;not null" json:"routineItemId"`
84 ExerciseID uint `gorm:"index;not null" json:"exerciseId"`
85 OrderIndex int `gorm:"not null" json:"orderIndex"`
86 CreatedAt time.Time `json:"createdAt"`
87 UpdatedAt time.Time `json:"updatedAt"`
88
89 RoutineItem RoutineItem `json:"-"`
90 Exercise Exercise `json:"exercise"`
91 Sets []Set `json:"sets"`
92}
93
94// Set represents a planned set within an exercise
95type Set struct {
96 ID uint `gorm:"primaryKey" json:"id"`
97 ExerciseItemID uint `gorm:"index;not null" json:"exerciseItemId"`
98 Reps int `json:"reps"`
99 Weight float64 `json:"weight"`
100 Duration int `json:"duration"` // In seconds
101 OrderIndex int `gorm:"not null" json:"orderIndex"`
102 CreatedAt time.Time `json:"createdAt"`
103 UpdatedAt time.Time `json:"updatedAt"`
104
105 ExerciseItem ExerciseItem `json:"-"`
106}
107
108// ===== RECORD MODELS (for actual workout completion) =====
109
110// RecordRoutine records a completed workout session
111type RecordRoutine struct {
112 ID uint `gorm:"primaryKey" json:"id"`
113 RoutineID uint `gorm:"index;not null" json:"routineId"`
114 Duration *uint `json:"duration"` // In seconds
115 CreatedAt time.Time `json:"createdAt"`
116 UpdatedAt time.Time `json:"updatedAt"`
117
118 Routine Routine `json:"routine"`
119 RecordItems []RecordItem `json:"recordItems"`
120}
121
122// RecordItem records completion of a routine item (exercise or superset)
123type RecordItem struct {
124 ID uint `gorm:"primaryKey" json:"id"`
125 RecordRoutineID uint `gorm:"index;not null" json:"recordRoutineId"`
126 RoutineItemID uint `gorm:"index;not null" json:"routineItemId"`
127 Duration *uint `json:"duration"` // In seconds
128 ActualRestTime *int `json:"actualRestTime"` // In seconds
129 OrderIndex int `gorm:"not null" json:"orderIndex"`
130 CreatedAt time.Time `json:"createdAt"`
131 UpdatedAt time.Time `json:"updatedAt"`
132
133 RecordRoutine RecordRoutine `json:"-"`
134 RoutineItem RoutineItem `json:"routineItem"`
135 RecordExerciseItems []RecordExerciseItem `json:"recordExerciseItems"`
136}
137
138// RecordExerciseItem records completion of an exercise within a routine item
139type RecordExerciseItem struct {
140 ID uint `gorm:"primaryKey" json:"id"`
141 RecordItemID uint `gorm:"index;not null" json:"recordItemId"`
142 ExerciseItemID uint `gorm:"index;not null" json:"exerciseItemId"`
143 OrderIndex int `gorm:"not null" json:"orderIndex"`
144 CreatedAt time.Time `json:"createdAt"`
145 UpdatedAt time.Time `json:"updatedAt"`
146
147 RecordItem RecordItem `json:"-"`
148 ExerciseItem ExerciseItem `json:"exerciseItem"`
149 RecordSets []RecordSet `json:"recordSets"`
150}
151
152// RecordSet records completion of an actual set
153type RecordSet struct {
154 ID uint `gorm:"primaryKey" json:"id"`
155 RecordExerciseItemID uint `gorm:"index;not null" json:"recordExerciseItemId"`
156 SetID uint `gorm:"index;not null" json:"setId"`
157 ActualReps int `json:"actualReps"`
158 ActualWeight float64 `json:"actualWeight"`
159 ActualDuration int `json:"actualDuration"` // In seconds
160 CompletedAt time.Time `gorm:"not null" json:"completedAt"`
161 OrderIndex int `gorm:"not null" json:"orderIndex"`
162 CreatedAt time.Time `json:"createdAt"`
163 UpdatedAt time.Time `json:"updatedAt"`
164
165 RecordExerciseItem RecordExerciseItem `json:"-"`
166 Set Set `json:"set"`
167}
168
169// InitializeDB creates and initializes the SQLite database with all models
170func InitializeDB() (db *Database, err error) {
171 // Create the data directory if it doesn't exist
172 if _, err = os.Stat(dbDir); os.IsNotExist(err) {
173 err = os.MkdirAll(dbDir, 0755)
174 if err != nil {
175 return
176 }
177 }
178
179 dbPath := filepath.Join(dbDir, dbName)
180
181 // Set up logger for GORM
182 newLogger := logger.New(
183 log.New(os.Stdout, "\r\n", log.LstdFlags),
184 logger.Config{
185 SlowThreshold: time.Second,
186 LogLevel: logger.Info,
187 Colorful: true,
188 },
189 )
190
191 dialector := sqlite.Open(dbPath + "?_pragma=foreign_keys(1)")
192 config := &gorm.Config{Logger: newLogger}
193
194 // Open connection to the database
195 conn, err := gorm.Open(dialector, config)
196 if err != nil {
197 return
198 }
199
200 // Get the underlying SQL database to set connection parameters
201 sqlDB, err := conn.DB()
202 if err != nil {
203 return nil, err
204 }
205
206 // Set connection pool settings
207 sqlDB.SetMaxIdleConns(10)
208 sqlDB.SetMaxOpenConns(100)
209 sqlDB.SetConnMaxLifetime(time.Hour)
210
211 // Auto migrate the models in correct order
212 err = conn.AutoMigrate(
213 &User{},
214 &Muscle{},
215 &Exercise{},
216 &Routine{},
217 &RoutineItem{},
218 &ExerciseItem{},
219 &Set{},
220 &RecordRoutine{},
221 &RecordItem{},
222 &RecordExerciseItem{},
223 &RecordSet{},
224 )
225 if err != nil {
226 return
227 }
228
229 db = &Database{conn}
230
231 // Ensure initial data is present
232 err = db.CheckInitialData()
233 if err != nil {
234 return nil, err
235 }
236
237 return db, nil
238}
239
240// Helper methods for creating and querying routines
241
242// CreateRoutineWithData creates a routine with all nested data
243func (db *Database) CreateRoutineWithData(routine *Routine) error {
244 return db.Create(routine).Error
245}
246
247// GetRoutineWithItems retrieves a routine with all its nested data
248func (db *Database) GetRoutineWithItems(routineID uint) (*Routine, error) {
249 var routine Routine
250 err := db.Preload("Items.ExerciseItems.Exercise.PrimaryMuscles").
251 Preload("Items.ExerciseItems.Exercise.SecondaryMuscles").
252 Preload("Items.ExerciseItems.Sets").
253 Order("Items.order_index, Items.ExerciseItems.order_index, Items.ExerciseItems.Sets.order_index").
254 First(&routine, routineID).Error
255
256 return &routine, err
257}
258
259// GetRecordRoutineWithData retrieves a completed workout with all nested data
260func (db *Database) GetRecordRoutineWithData(recordID uint) (*RecordRoutine, error) {
261 var record RecordRoutine
262 err := db.Preload("Routine").
263 Preload("RecordItems.RoutineItem").
264 Preload("RecordItems.RecordExerciseItems.ExerciseItem.Exercise.PrimaryMuscles").
265 Preload("RecordItems.RecordExerciseItems.ExerciseItem.Exercise.SecondaryMuscles").
266 Preload("RecordItems.RecordExerciseItems.RecordSets.Set").
267 Order("RecordItems.order_index, RecordItems.RecordExerciseItems.order_index, RecordItems.RecordExerciseItems.RecordSets.order_index").
268 First(&record, recordID).Error
269
270 return &record, err
271}