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 *string `json:"primaryMuscles"`
41 SecondaryMuscles *string `json:"secondaryMuscles"`
42
43 CreatedAt time.Time `json:"createdAt"`
44 UpdatedAt time.Time `json:"updatedAt"`
45}
46
47// Routine represents a workout routine blueprint
48type Routine struct {
49 ID uint `gorm:"primaryKey" json:"id"`
50 Name string `gorm:"size:100;not null" json:"name"`
51 Description string `gorm:"size:500" json:"description"`
52 CreatedAt time.Time `json:"createdAt"`
53 UpdatedAt time.Time `json:"updatedAt"`
54
55 Items []RoutineItem `json:"items"`
56}
57
58// RoutineItem can be either a single exercise or a superset
59type RoutineItem struct {
60 ID uint `gorm:"primaryKey" json:"id"`
61 RoutineID uint `gorm:"index;not null" json:"routineId"`
62 Type string `gorm:"size:20;not null" json:"type"` // "exercise" or "superset"
63 RestTime int `gorm:"default:0" json:"restTime"` // In seconds
64 OrderIndex int `gorm:"not null" json:"orderIndex"`
65 CreatedAt time.Time `json:"createdAt"`
66 UpdatedAt time.Time `json:"updatedAt"`
67
68 Routine Routine `json:"-"`
69 ExerciseItems []ExerciseItem `json:"exerciseItems,omitempty"` // For both single exercises and superset items
70}
71
72// ExerciseItem represents an exercise within a routine item (could be standalone or part of superset)
73type ExerciseItem struct {
74 ID uint `gorm:"primaryKey" json:"id"`
75 RoutineItemID uint `gorm:"index;not null" json:"routineItemId"`
76 ExerciseID uint `gorm:"index;not null" json:"exerciseId"`
77 OrderIndex int `gorm:"not null" json:"orderIndex"`
78 CreatedAt time.Time `json:"createdAt"`
79 UpdatedAt time.Time `json:"updatedAt"`
80
81 RoutineItem RoutineItem `json:"-"`
82 Exercise Exercise `json:"exercise"`
83 Sets []Set `json:"sets"`
84}
85
86// Set represents a planned set within an exercise
87type Set struct {
88 ID uint `gorm:"primaryKey" json:"id"`
89 ExerciseItemID uint `gorm:"index;not null" json:"exerciseItemId"`
90 Reps int `json:"reps"`
91 Weight float64 `json:"weight"`
92 Duration int `json:"duration"` // In seconds
93 OrderIndex int `gorm:"not null" json:"orderIndex"`
94 CreatedAt time.Time `json:"createdAt"`
95 UpdatedAt time.Time `json:"updatedAt"`
96
97 ExerciseItem ExerciseItem `json:"-"`
98}
99
100// ===== RECORD MODELS (for actual workout completion) =====
101
102// RecordRoutine records a completed workout session
103type RecordRoutine struct {
104 ID uint `gorm:"primaryKey" json:"id"`
105 RoutineID uint `gorm:"index;not null" json:"routineId"`
106 Duration *uint `json:"duration"` // In seconds
107 CreatedAt time.Time `json:"createdAt"`
108 UpdatedAt time.Time `json:"updatedAt"`
109
110 Routine Routine `json:"routine"`
111 RecordItems []RecordItem `json:"recordItems"`
112}
113
114// RecordItem records completion of a routine item (exercise or superset)
115type RecordItem struct {
116 ID uint `gorm:"primaryKey" json:"id"`
117 RecordRoutineID uint `gorm:"index;not null" json:"recordRoutineId"`
118 RoutineItemID uint `gorm:"index;not null" json:"routineItemId"`
119 Duration *uint `json:"duration"` // In seconds
120 ActualRestTime *int `json:"actualRestTime"` // In seconds
121 OrderIndex int `gorm:"not null" json:"orderIndex"`
122 CreatedAt time.Time `json:"createdAt"`
123 UpdatedAt time.Time `json:"updatedAt"`
124
125 RecordRoutine RecordRoutine `json:"-"`
126 RoutineItem RoutineItem `json:"routineItem"`
127 RecordExerciseItems []RecordExerciseItem `json:"recordExerciseItems"`
128}
129
130// RecordExerciseItem records completion of an exercise within a routine item
131type RecordExerciseItem struct {
132 ID uint `gorm:"primaryKey" json:"id"`
133 RecordItemID uint `gorm:"index;not null" json:"recordItemId"`
134 ExerciseItemID uint `gorm:"index;not null" json:"exerciseItemId"`
135 OrderIndex int `gorm:"not null" json:"orderIndex"`
136 CreatedAt time.Time `json:"createdAt"`
137 UpdatedAt time.Time `json:"updatedAt"`
138
139 RecordItem RecordItem `json:"-"`
140 ExerciseItem ExerciseItem `json:"exerciseItem"`
141 RecordSets []RecordSet `json:"recordSets"`
142}
143
144// RecordSet records completion of an actual set
145type RecordSet struct {
146 ID uint `gorm:"primaryKey" json:"id"`
147 RecordExerciseItemID uint `gorm:"index;not null" json:"recordExerciseItemId"`
148 SetID uint `gorm:"index;not null" json:"setId"`
149 ActualReps int `json:"actualReps"`
150 ActualWeight float64 `json:"actualWeight"`
151 ActualDuration int `json:"actualDuration"` // In seconds
152 CompletedAt time.Time `gorm:"not null" json:"completedAt"`
153 OrderIndex int `gorm:"not null" json:"orderIndex"`
154 CreatedAt time.Time `json:"createdAt"`
155 UpdatedAt time.Time `json:"updatedAt"`
156
157 RecordExerciseItem RecordExerciseItem `json:"-"`
158 Set Set `json:"set"`
159}
160
161// InitializeDB creates and initializes the SQLite database with all models
162func InitializeDB() (db *Database, err error) {
163 // Create the data directory if it doesn't exist
164 if _, err = os.Stat(dbDir); os.IsNotExist(err) {
165 err = os.MkdirAll(dbDir, 0755)
166 if err != nil {
167 return
168 }
169 }
170
171 dbPath := filepath.Join(dbDir, dbName)
172
173 // Set up logger for GORM
174 newLogger := logger.New(
175 log.New(os.Stdout, "\r\n", log.LstdFlags),
176 logger.Config{
177 SlowThreshold: time.Second,
178 LogLevel: logger.Info,
179 Colorful: true,
180 },
181 )
182
183 dialector := sqlite.Open(dbPath + "?_pragma=foreign_keys(1)")
184 config := &gorm.Config{Logger: newLogger}
185
186 // Open connection to the database
187 conn, err := gorm.Open(dialector, config)
188 if err != nil {
189 return
190 }
191
192 // Get the underlying SQL database to set connection parameters
193 sqlDB, err := conn.DB()
194 if err != nil {
195 return nil, err
196 }
197
198 // Set connection pool settings
199 sqlDB.SetMaxIdleConns(10)
200 sqlDB.SetMaxOpenConns(100)
201 sqlDB.SetConnMaxLifetime(time.Hour)
202
203 // Auto migrate the models in correct order
204 err = conn.AutoMigrate(
205 &User{},
206 &Exercise{},
207 &Routine{},
208 &RoutineItem{},
209 &ExerciseItem{},
210 &Set{},
211 &RecordRoutine{},
212 &RecordItem{},
213 &RecordExerciseItem{},
214 &RecordSet{},
215 )
216 if err != nil {
217 return
218 }
219
220 db = &Database{conn}
221
222 // Ensure initial data is present
223 err = db.CheckInitialData()
224 if err != nil {
225 return nil, err
226 }
227
228 return db, nil
229}