all repos — go-lift @ 387721fc5e90ba268efbae885c9baf8e0a543f28

Lightweight workout tracker prototype..

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}