all repos — go-lift @ 092e7440440b8459d82fb90c05c005f9f38e3c51

Lightweight workout tracker prototype..

src/api/crud.go (view raw)

  1package api
  2
  3import (
  4	"encoding/json"
  5	"errors"
  6	"net/http"
  7
  8	"github.com/birabittoh/go-lift/src/database"
  9	"gorm.io/gorm"
 10)
 11
 12type WorkoutStats struct {
 13	TotalWorkouts        int64 `json:"totalWorkouts"`
 14	TotalMinutes         int   `json:"totalMinutes"`
 15	TotalExercises       int64 `json:"totalExercises"`
 16	MostFrequentExercise *struct {
 17		Name  string `json:"name"`
 18		Count int    `json:"count"`
 19	} `json:"mostFrequentExercise,omitempty"`
 20	MostFrequentRoutine *struct {
 21		Name  string `json:"name"`
 22		Count int    `json:"count"`
 23	} `json:"mostFrequentRoutine,omitempty"`
 24	RecentWorkouts []database.RecordRoutine `json:"recentWorkouts"`
 25}
 26
 27// User handlers
 28func getUserHandler(db *gorm.DB) http.HandlerFunc {
 29	return func(w http.ResponseWriter, r *http.Request) {
 30		id := r.PathValue("id")
 31		var user database.User
 32		if err := db.First(&user, id).Error; err != nil {
 33			if errors.Is(err, gorm.ErrRecordNotFound) {
 34				jsonError(w, http.StatusNotFound, "User not found")
 35				return
 36			}
 37			jsonError(w, http.StatusInternalServerError, "Failed to fetch user: "+err.Error())
 38			return
 39		}
 40		jsonResponse(w, http.StatusOK, user)
 41	}
 42}
 43
 44func updateUserHandler(db *gorm.DB) http.HandlerFunc {
 45	return func(w http.ResponseWriter, r *http.Request) {
 46		id := r.PathValue("id")
 47		var user database.User
 48		if err := db.First(&user, id).Error; err != nil {
 49			if errors.Is(err, gorm.ErrRecordNotFound) {
 50				jsonError(w, http.StatusNotFound, "User not found")
 51				return
 52			}
 53			jsonError(w, http.StatusInternalServerError, "Failed to fetch user: "+err.Error())
 54			return
 55		}
 56
 57		if err := json.NewDecoder(r.Body).Decode(&user); err != nil {
 58			jsonError(w, http.StatusBadRequest, "Invalid request body: "+err.Error())
 59			return
 60		}
 61		if err := db.Save(&user).Error; err != nil {
 62			jsonError(w, http.StatusInternalServerError, "Failed to update user: "+err.Error())
 63			return
 64		}
 65		jsonResponse(w, http.StatusOK, user)
 66	}
 67}
 68
 69// Routines handlers
 70func getRoutinesHandler(db *gorm.DB) http.HandlerFunc {
 71	return func(w http.ResponseWriter, r *http.Request) {
 72		var routines []database.Routine
 73		result := db.
 74			Preload("RoutineItems").
 75			Preload("RoutineItems.Exercises").
 76			Preload("RoutineItems.Supersets").
 77			Preload("RoutineItems.Supersets.PrimaryExercise").
 78			Preload("RoutineItems.Supersets.SecondaryExercise").
 79			Find(&routines)
 80		if result.Error != nil {
 81			jsonError(w, http.StatusInternalServerError, "Failed to fetch routines: "+result.Error.Error())
 82			return
 83		}
 84		jsonResponse(w, http.StatusOK, routines)
 85	}
 86}
 87
 88func getRoutineHandler(db *gorm.DB) http.HandlerFunc {
 89	return func(w http.ResponseWriter, r *http.Request) {
 90		id := r.PathValue("id")
 91		var routine database.Routine
 92		result := db.
 93			Preload("RoutineItems.Exercises").
 94			Preload("RoutineItems.Supersets").
 95			Preload("RoutineItems.Supersets.PrimaryExercise").
 96			Preload("RoutineItems.Supersets.SecondaryExercise").
 97			First(&routine, id)
 98		if result.Error != nil {
 99			if errors.Is(result.Error, gorm.ErrRecordNotFound) {
100				jsonError(w, http.StatusNotFound, "Routine not found")
101				return
102			}
103			jsonError(w, http.StatusInternalServerError, "Failed to fetch routine: "+result.Error.Error())
104			return
105		}
106		jsonResponse(w, http.StatusOK, routine)
107	}
108}
109
110func createRoutineHandler(db *gorm.DB) http.HandlerFunc {
111	return func(w http.ResponseWriter, r *http.Request) {
112		var routine database.Routine
113		if err := json.NewDecoder(r.Body).Decode(&routine); err != nil {
114			jsonError(w, http.StatusBadRequest, "Invalid request body: "+err.Error())
115			return
116		}
117
118		if err := db.Create(&routine).Error; err != nil {
119			jsonError(w, http.StatusInternalServerError, "Failed to create routine: "+err.Error())
120			return
121		}
122
123		// Reload with associations
124		db.Preload("Exercises").Preload("Supersets").Preload("Supersets.Sets").First(&routine, routine.ID)
125
126		jsonResponse(w, http.StatusCreated, routine)
127	}
128}
129
130func updateRoutineHandler(db *gorm.DB) http.HandlerFunc {
131	return func(w http.ResponseWriter, r *http.Request) {
132		id := r.PathValue("id")
133		var routine database.Routine
134
135		// Check if exists
136		if err := db.First(&routine, id).Error; err != nil {
137			if errors.Is(err, gorm.ErrRecordNotFound) {
138				jsonError(w, http.StatusNotFound, "Routine not found")
139				return
140			}
141			jsonError(w, http.StatusInternalServerError, "Database error: "+err.Error())
142			return
143		}
144
145		// Parse update data
146		if err := json.NewDecoder(r.Body).Decode(&routine); err != nil {
147			jsonError(w, http.StatusBadRequest, "Invalid request body: "+err.Error())
148			return
149		}
150
151		// Save with associations
152		if err := db.Session(&gorm.Session{FullSaveAssociations: true}).Save(&routine).Error; err != nil {
153			jsonError(w, http.StatusInternalServerError, "Failed to update routine: "+err.Error())
154			return
155		}
156
157		// Reload complete data
158		db.Preload("Exercises").Preload("Supersets").Preload("Supersets.Sets").First(&routine, id)
159		jsonResponse(w, http.StatusOK, routine)
160	}
161}
162
163func deleteRoutineHandler(db *gorm.DB) http.HandlerFunc {
164	return func(w http.ResponseWriter, r *http.Request) {
165		id := r.PathValue("id")
166		if err := db.Delete(&database.Routine{}, id).Error; err != nil {
167			jsonError(w, http.StatusInternalServerError, "Failed to delete routine: "+err.Error())
168			return
169		}
170		jsonResponse(w, http.StatusOK, map[string]string{"message": "Routine deleted successfully"})
171	}
172}
173
174// Exercises handlers
175func getExercisesHandler(db *gorm.DB) http.HandlerFunc {
176	return func(w http.ResponseWriter, r *http.Request) {
177		var exercises []database.Exercise
178		if err := db.Find(&exercises).Error; err != nil {
179			jsonError(w, http.StatusInternalServerError, "Failed to fetch exercises: "+err.Error())
180			return
181		}
182		jsonResponse(w, http.StatusOK, exercises)
183	}
184}
185
186func getExerciseHandler(db *gorm.DB) http.HandlerFunc {
187	return func(w http.ResponseWriter, r *http.Request) {
188		id := r.PathValue("id")
189		var exercise database.Exercise
190		if err := db.First(&exercise, id).Error; err != nil {
191			if errors.Is(err, gorm.ErrRecordNotFound) {
192				jsonError(w, http.StatusNotFound, "Exercise not found")
193				return
194			}
195			jsonError(w, http.StatusInternalServerError, "Failed to fetch exercise: "+err.Error())
196			return
197		}
198		jsonResponse(w, http.StatusOK, exercise)
199	}
200}
201
202func createExerciseHandler(db *gorm.DB) http.HandlerFunc {
203	return func(w http.ResponseWriter, r *http.Request) {
204		var exercise database.Exercise
205		if err := json.NewDecoder(r.Body).Decode(&exercise); err != nil {
206			jsonError(w, http.StatusBadRequest, "Invalid request body: "+err.Error())
207			return
208		}
209
210		if err := db.Create(&exercise).Error; err != nil {
211			jsonError(w, http.StatusInternalServerError, "Failed to create exercise: "+err.Error())
212			return
213		}
214
215		jsonResponse(w, http.StatusCreated, exercise)
216	}
217}
218
219func upsertExercisesHandler(dbStruct *database.Database) http.HandlerFunc {
220	return func(w http.ResponseWriter, r *http.Request) {
221		if err := dbStruct.UpdateExercises(); err != nil {
222			jsonError(w, http.StatusInternalServerError, "Failed to update exercises: "+err.Error())
223			return
224		}
225
226		jsonResponse(w, http.StatusOK, map[string]string{"message": "Exercises updated successfully"})
227	}
228}
229
230func updateExerciseHandler(db *gorm.DB) http.HandlerFunc {
231	return func(w http.ResponseWriter, r *http.Request) {
232		id := r.PathValue("id")
233
234		// Verify exercise exists
235		var exercise database.Exercise
236		if err := db.First(&exercise, id).Error; err != nil {
237			if errors.Is(err, gorm.ErrRecordNotFound) {
238				jsonError(w, http.StatusNotFound, "Exercise not found")
239				return
240			}
241			jsonError(w, http.StatusInternalServerError, "Database error: "+err.Error())
242			return
243		}
244
245		// Parse update data
246		if err := json.NewDecoder(r.Body).Decode(&exercise); err != nil {
247			jsonError(w, http.StatusBadRequest, "Invalid request body: "+err.Error())
248			return
249		}
250
251		if err := db.Save(&exercise).Error; err != nil {
252			jsonError(w, http.StatusInternalServerError, "Failed to update exercise: "+err.Error())
253			return
254		}
255
256		jsonResponse(w, http.StatusOK, exercise)
257	}
258}
259
260func deleteExerciseHandler(db *gorm.DB) http.HandlerFunc {
261	return func(w http.ResponseWriter, r *http.Request) {
262		id := r.PathValue("id")
263		if err := db.Delete(&database.Exercise{}, id).Error; err != nil {
264			jsonError(w, http.StatusInternalServerError, "Failed to delete exercise: "+err.Error())
265			return
266		}
267		jsonResponse(w, http.StatusOK, map[string]string{"message": "Exercise deleted successfully"})
268	}
269}
270
271// RecordRoutines handlers
272func getRecordRoutinesHandler(db *gorm.DB) http.HandlerFunc {
273	return func(w http.ResponseWriter, r *http.Request) {
274		var records []database.RecordRoutine
275		result := db.Preload("RecordExercises").Preload("Routine.Exercises").Preload("Routine.Supersets").Find(&records)
276		if result.Error != nil {
277			jsonError(w, http.StatusInternalServerError, "Failed to fetch record routines: "+result.Error.Error())
278			return
279		}
280		jsonResponse(w, http.StatusOK, records)
281	}
282}
283
284func getRecordRoutineHandler(db *gorm.DB) http.HandlerFunc {
285	return func(w http.ResponseWriter, r *http.Request) {
286		id := r.PathValue("id")
287		var record database.RecordRoutine
288		result := db.Preload("Routine").Preload("Routine.Exercises").Preload("Routine.Supersets").First(&record, id)
289		if result.Error != nil {
290			if errors.Is(result.Error, gorm.ErrRecordNotFound) {
291				jsonError(w, http.StatusNotFound, "Record routine not found")
292				return
293			}
294			jsonError(w, http.StatusInternalServerError, "Failed to fetch record routine: "+result.Error.Error())
295			return
296		}
297		jsonResponse(w, http.StatusOK, record)
298	}
299}
300
301func createRecordRoutineHandler(db *gorm.DB) http.HandlerFunc {
302	return func(w http.ResponseWriter, r *http.Request) {
303		var record database.RecordRoutine
304		if err := json.NewDecoder(r.Body).Decode(&record); err != nil {
305			jsonError(w, http.StatusBadRequest, "Invalid request body: "+err.Error())
306			return
307		}
308
309		if err := db.Create(&record).Error; err != nil {
310			jsonError(w, http.StatusInternalServerError, "Failed to create record routine: "+err.Error())
311			return
312		}
313
314		// Reload with associations
315		db.Preload("Routine").Preload("Routine.Exercises").Preload("Routine.Supersets").First(&record, record.ID)
316
317		jsonResponse(w, http.StatusCreated, record)
318	}
319}
320
321func updateRecordRoutineHandler(db *gorm.DB) http.HandlerFunc {
322	return func(w http.ResponseWriter, r *http.Request) {
323		id := r.PathValue("id")
324		var record database.RecordRoutine
325
326		// Check if exists
327		if err := db.First(&record, id).Error; err != nil {
328			if errors.Is(err, gorm.ErrRecordNotFound) {
329				jsonError(w, http.StatusNotFound, "Record routine not found")
330				return
331			}
332			jsonError(w, http.StatusInternalServerError, "Database error: "+err.Error())
333			return
334		}
335
336		// Parse update data
337		if err := json.NewDecoder(r.Body).Decode(&record); err != nil {
338			jsonError(w, http.StatusBadRequest, "Invalid request body: "+err.Error())
339			return
340		}
341
342		// Save with associations
343		if err := db.Session(&gorm.Session{FullSaveAssociations: true}).Save(&record).Error; err != nil {
344			jsonError(w, http.StatusInternalServerError, "Failed to update record routine: "+err.Error())
345			return
346		}
347
348		// Reload complete data
349		db.Preload("Routine").Preload("Routine.Exercises").Preload("Routine.Supersets").First(&record, id)
350		jsonResponse(w, http.StatusOK, record)
351	}
352}
353
354func deleteRecordRoutineHandler(db *gorm.DB) http.HandlerFunc {
355	return func(w http.ResponseWriter, r *http.Request) {
356		id := r.PathValue("id")
357		if err := db.Delete(&database.RecordRoutine{}, id).Error; err != nil {
358			jsonError(w, http.StatusInternalServerError, "Failed to delete record routine: "+err.Error())
359			return
360		}
361		jsonResponse(w, http.StatusOK, map[string]string{"message": "Record routine deleted successfully"})
362	}
363}
364
365func getStatsHandler(db *gorm.DB) http.HandlerFunc {
366	return func(w http.ResponseWriter, r *http.Request) {
367		stats := WorkoutStats{}
368
369		// Get total workouts
370		if err := db.Model(&database.RecordRoutine{}).Count(&stats.TotalWorkouts).Error; err != nil {
371			jsonError(w, http.StatusInternalServerError, "Failed to count workouts: "+err.Error())
372			return
373		}
374
375		// Get total minutes
376		stats.TotalMinutes = 0
377
378		// Get total exercises
379		if err := db.Model(&database.RecordExercise{}).Count(&stats.TotalExercises).Error; err != nil {
380			jsonError(w, http.StatusInternalServerError, "Failed to count exercises: "+err.Error())
381			return
382		}
383
384		// Get most frequent exercise
385		var mostFrequentExercise struct {
386			Name  string `gorm:"column:name"`
387			Count int    `gorm:"column:count"`
388		}
389		exerciseQuery := db.Model(&database.RecordExercise{}).
390			Select("exercises.name, COUNT(*) as count").
391			Joins("JOIN exercises ON record_exercises.exercise_id = exercises.id").
392			Group("exercises.name").
393			Order("count DESC").
394			Limit(1)
395
396		if err := exerciseQuery.Scan(&mostFrequentExercise).Error; err == nil && mostFrequentExercise.Name != "" {
397			stats.MostFrequentExercise = &struct {
398				Name  string `json:"name"`
399				Count int    `json:"count"`
400			}{
401				Name:  mostFrequentExercise.Name,
402				Count: mostFrequentExercise.Count,
403			}
404		}
405
406		// Get most frequent routine
407		var mostFrequentRoutine struct {
408			Name  string `gorm:"column:name"`
409			Count int    `gorm:"column:count"`
410		}
411		routineQuery := db.Model(&database.RecordRoutine{}).
412			Select("routines.name, COUNT(*) as count").
413			Joins("JOIN routines ON record_routines.routine_id = routines.id").
414			Group("routines.name").
415			Order("count DESC").
416			Limit(1)
417
418		if err := routineQuery.Scan(&mostFrequentRoutine).Error; err == nil && mostFrequentRoutine.Name != "" {
419			stats.MostFrequentRoutine = &struct {
420				Name  string `json:"name"`
421				Count int    `json:"count"`
422			}{
423				Name:  mostFrequentRoutine.Name,
424				Count: mostFrequentRoutine.Count,
425			}
426		}
427
428		// Get recent workouts (last 5)
429		if err := db.
430			Preload("RecordRoutineItems").
431			Preload("Routine").
432			Order("created_at DESC").
433			Limit(5).
434			Find(&stats.RecentWorkouts).Error; err != nil {
435			jsonError(w, http.StatusInternalServerError, "Failed to fetch recent workouts: "+err.Error())
436			return
437		}
438
439		jsonResponse(w, http.StatusOK, stats)
440	}
441}