From 01f664423ffdb779c22bdf5ecda800dcaf4a1d54 Mon Sep 17 00:00:00 2001 From: "google-labs-jules[bot]" <161369871+google-labs-jules[bot]@users.noreply.github.com> Date: Tue, 23 Dec 2025 19:29:16 +0000 Subject: [PATCH] Fix expo-sqlite WASM build and add Data Access Layer - Update `metro.config.js` to treat .wasm files as assets to fix web build SyntaxError - Create `workout-logger/repositories/WorkoutRepository.js` - Create `workout-logger/repositories/ExerciseRepository.js` - Create `workout-logger/repositories/SetRepository.js` - Create `workout-logger/repositories/AnalyticsRepository.js` - Implements step 2 of the documentation (Data Access Layer) --- workout-logger/metro.config.js | 13 ++ .../repositories/AnalyticsRepository.js | 101 +++++++++++ .../repositories/ExerciseRepository.js | 113 ++++++++++++ workout-logger/repositories/SetRepository.js | 97 +++++++++++ .../repositories/WorkoutRepository.js | 164 ++++++++++++++++++ 5 files changed, 488 insertions(+) create mode 100644 workout-logger/metro.config.js create mode 100644 workout-logger/repositories/AnalyticsRepository.js create mode 100644 workout-logger/repositories/ExerciseRepository.js create mode 100644 workout-logger/repositories/SetRepository.js create mode 100644 workout-logger/repositories/WorkoutRepository.js diff --git a/workout-logger/metro.config.js b/workout-logger/metro.config.js new file mode 100644 index 0000000..7f01e1b --- /dev/null +++ b/workout-logger/metro.config.js @@ -0,0 +1,13 @@ +const { getDefaultConfig } = require('expo/metro-config'); + +const config = getDefaultConfig(__dirname); + +// Add wasm to assetExts so it can be resolved as a file asset (URL) +if (!config.resolver.assetExts.includes('wasm')) { + config.resolver.assetExts.push('wasm'); +} + +// Ensure wasm is NOT in sourceExts to prevent Metro from trying to parse it as JS +config.resolver.sourceExts = config.resolver.sourceExts.filter(ext => ext !== 'wasm'); + +module.exports = config; diff --git a/workout-logger/repositories/AnalyticsRepository.js b/workout-logger/repositories/AnalyticsRepository.js new file mode 100644 index 0000000..d8edf58 --- /dev/null +++ b/workout-logger/repositories/AnalyticsRepository.js @@ -0,0 +1,101 @@ +import DatabaseService from '../services/database'; + +class AnalyticsRepository { + /** + * Get volume by muscle group over time + */ + async getVolumeByMuscleGroup(startDate, endDate) { + return await DatabaseService.db.getAllAsync(` + SELECT + e.muscle_group, + SUM(s.weight_lbs * s.reps) as total_volume, + COUNT(DISTINCT w.id) as workout_count + FROM sets s + JOIN workout_exercises we ON s.workout_exercise_id = we.id + JOIN workouts w ON we.workout_id = w.id + JOIN exercises e ON we.exercise_id = e.id + WHERE w.date BETWEEN ? AND ? AND s.is_completed = 1 + GROUP BY e.muscle_group + ORDER BY total_volume DESC + `, [startDate, endDate]); + } + + /** + * Get strength progression for an exercise + */ + async getStrengthProgression(exerciseId, limit = 20) { + return await DatabaseService.db.getAllAsync(` + SELECT + w.date, + MAX(s.weight_lbs) as max_weight, + AVG(s.weight_lbs) as avg_weight, + MAX(s.reps) as max_reps, + SUM(s.weight_lbs * s.reps) as volume + FROM sets s + JOIN workout_exercises we ON s.workout_exercise_id = we.id + JOIN workouts w ON we.workout_id = w.id + WHERE we.exercise_id = ? AND s.is_completed = 1 + GROUP BY w.date + ORDER BY w.date DESC + LIMIT ? + `, [exerciseId, limit]); + } + + /** + * Get workout frequency by week + */ + async getWorkoutFrequency(weeks = 12) { + return await DatabaseService.db.getAllAsync(` + SELECT + strftime('%Y-%W', date) as week, + COUNT(*) as workout_count + FROM workouts + WHERE date >= date('now', '-${weeks} weeks') + GROUP BY week + ORDER BY week DESC + `); + } + + /** + * Get personal records for all exercises + */ + async getPersonalRecords() { + return await DatabaseService.db.getAllAsync(` + SELECT + e.name, + e.muscle_group, + MAX(s.weight_lbs) as max_weight, + s.reps as reps_at_max, + w.date + FROM sets s + JOIN workout_exercises we ON s.workout_exercise_id = we.id + JOIN exercises e ON we.exercise_id = e.id + JOIN workouts w ON we.workout_id = w.id + WHERE s.is_completed = 1 + GROUP BY e.id + HAVING s.weight_lbs = MAX(s.weight_lbs) + `); + } + + /** + * Calculate volume progression (for progressive overload tracking) + */ + async getVolumeProgression(muscleGroup, weeks = 8) { + return await DatabaseService.db.getAllAsync(` + SELECT + strftime('%Y-%W', w.date) as week, + SUM(s.weight_lbs * s.reps) as total_volume + FROM sets s + JOIN workout_exercises we ON s.workout_exercise_id = we.id + JOIN workouts w ON we.workout_id = w.id + JOIN exercises e ON we.exercise_id = e.id + WHERE e.muscle_group = ? + AND s.is_completed = 1 + AND w.date >= date('now', '-${weeks} weeks') + GROUP BY week + ORDER BY week ASC + `, [muscleGroup]); + } +} + +export default new AnalyticsRepository(); diff --git a/workout-logger/repositories/ExerciseRepository.js b/workout-logger/repositories/ExerciseRepository.js new file mode 100644 index 0000000..b127d43 --- /dev/null +++ b/workout-logger/repositories/ExerciseRepository.js @@ -0,0 +1,113 @@ +import DatabaseService from '../services/database'; + +class ExerciseRepository { + /** + * Get all exercises from library + */ + async getAllExercises({ muscleGroup = null, searchQuery = null } = {}) { + let query = 'SELECT * FROM exercises WHERE 1=1'; + const params = []; + + if (muscleGroup) { + query += ' AND muscle_group = ?'; + params.push(muscleGroup); + } + + if (searchQuery) { + query += ' AND name LIKE ?'; + params.push(`%${searchQuery}%`); + } + + query += ' ORDER BY name'; + + return await DatabaseService.db.getAllAsync(query, params); + } + + /** + * Get exercise by ID + */ + async getExerciseById(exerciseId) { + return await DatabaseService.db.getFirstAsync( + 'SELECT * FROM exercises WHERE id = ?', + [exerciseId] + ); + } + + /** + * Create custom exercise + */ + async createExercise({ name, muscleGroup, equipmentType, description, imageUrl }) { + const result = await DatabaseService.db.runAsync( + `INSERT INTO exercises (name, muscle_group, equipment_type, description, image_url, is_custom) + VALUES (?, ?, ?, ?, ?, 1)`, + [name, muscleGroup, equipmentType, description, imageUrl] + ); + return result.lastInsertRowId; + } + + /** + * Add exercise to workout + */ + async addExerciseToWorkout(workoutId, exerciseId, orderIndex) { + const result = await DatabaseService.db.runAsync( + `INSERT INTO workout_exercises (workout_id, exercise_id, order_index) VALUES (?, ?, ?)`, + [workoutId, exerciseId, orderIndex] + ); + return result.lastInsertRowId; + } + + /** + * Remove exercise from workout + */ + async removeExerciseFromWorkout(workoutExerciseId) { + await DatabaseService.db.runAsync( + `DELETE FROM workout_exercises WHERE id = ?`, + [workoutExerciseId] + ); + } + + /** + * Reorder exercises in workout + */ + async reorderExercises(workoutId, exerciseOrders) { + // exerciseOrders is array of { workoutExerciseId, orderIndex } + for (const { workoutExerciseId, orderIndex } of exerciseOrders) { + await DatabaseService.db.runAsync( + `UPDATE workout_exercises SET order_index = ? WHERE id = ? AND workout_id = ?`, + [orderIndex, workoutExerciseId, workoutId] + ); + } + } + + /** + * Get exercise history (previous performances) + */ + async getExerciseHistory(exerciseId, limit = 10) { + return await DatabaseService.db.getAllAsync(` + SELECT + w.date, + w.name as workout_name, + we.id as workout_exercise_id, + s.weight_lbs, + s.reps, + s.set_number + FROM workout_exercises we + JOIN workouts w ON we.workout_id = w.id + JOIN sets s ON we.id = s.workout_exercise_id + WHERE we.exercise_id = ? AND s.is_completed = 1 + ORDER BY w.date DESC, s.set_number + LIMIT ? + `, [exerciseId, limit]); + } + + /** + * Get all muscle groups + */ + async getMuscleGroups() { + return await DatabaseService.db.getAllAsync(` + SELECT DISTINCT muscle_group FROM exercises ORDER BY muscle_group + `); + } +} + +export default new ExerciseRepository(); diff --git a/workout-logger/repositories/SetRepository.js b/workout-logger/repositories/SetRepository.js new file mode 100644 index 0000000..1c9fdf9 --- /dev/null +++ b/workout-logger/repositories/SetRepository.js @@ -0,0 +1,97 @@ +import DatabaseService from '../services/database'; + +class SetRepository { + /** + * Add a set to an exercise + */ + async addSet(workoutExerciseId, setNumber, weightLbs = null, reps = null) { + const result = await DatabaseService.db.runAsync( + `INSERT INTO sets (workout_exercise_id, set_number, weight_lbs, reps) VALUES (?, ?, ?, ?)`, + [workoutExerciseId, setNumber, weightLbs, reps] + ); + return result.lastInsertRowId; + } + + /** + * Update set data + */ + async updateSet(setId, { weightLbs, reps, isCompleted, rpe, notes }) { + const updates = []; + const params = []; + + if (weightLbs !== undefined) { + updates.push('weight_lbs = ?'); + params.push(weightLbs); + } + if (reps !== undefined) { + updates.push('reps = ?'); + params.push(reps); + } + if (isCompleted !== undefined) { + updates.push('is_completed = ?'); + params.push(isCompleted ? 1 : 0); + } + if (rpe !== undefined) { + updates.push('rpe = ?'); + params.push(rpe); + } + if (notes !== undefined) { + updates.push('notes = ?'); + params.push(notes); + } + + if (updates.length === 0) return; + + params.push(setId); + await DatabaseService.db.runAsync( + `UPDATE sets SET ${updates.join(', ')} WHERE id = ?`, + params + ); + } + + /** + * Delete a set + */ + async deleteSet(setId) { + await DatabaseService.db.runAsync('DELETE FROM sets WHERE id = ?', [setId]); + } + + /** + * Get all sets for a workout exercise + */ + async getSetsByWorkoutExercise(workoutExerciseId) { + return await DatabaseService.db.getAllAsync( + 'SELECT * FROM sets WHERE workout_exercise_id = ? ORDER BY set_number', + [workoutExerciseId] + ); + } + + /** + * Calculate volume for an exercise + */ + async calculateExerciseVolume(workoutExerciseId) { + const result = await DatabaseService.db.getFirstAsync(` + SELECT SUM(weight_lbs * reps) as total_volume + FROM sets + WHERE workout_exercise_id = ? AND is_completed = 1 + `, [workoutExerciseId]); + + return result?.total_volume || 0; + } + + /** + * Calculate total volume for a workout + */ + async calculateWorkoutVolume(workoutId) { + const result = await DatabaseService.db.getFirstAsync(` + SELECT SUM(s.weight_lbs * s.reps) as total_volume + FROM sets s + JOIN workout_exercises we ON s.workout_exercise_id = we.id + WHERE we.workout_id = ? AND s.is_completed = 1 + `, [workoutId]); + + return result?.total_volume || 0; + } +} + +export default new SetRepository(); diff --git a/workout-logger/repositories/WorkoutRepository.js b/workout-logger/repositories/WorkoutRepository.js new file mode 100644 index 0000000..ac294a0 --- /dev/null +++ b/workout-logger/repositories/WorkoutRepository.js @@ -0,0 +1,164 @@ +import DatabaseService from '../services/database'; + +class WorkoutRepository { + /** + * Create a new workout + * @returns {Promise} - The ID of the created workout + */ + async createWorkout({ name, date, startTime }) { + const result = await DatabaseService.db.runAsync( + `INSERT INTO workouts (name, date, start_time) VALUES (?, ?, ?)`, + [name, date, startTime] + ); + return result.lastInsertRowId; + } + + /** + * Get a workout by ID with all exercises and sets + */ + async getWorkoutById(workoutId) { + const workout = await DatabaseService.db.getFirstAsync( + `SELECT * FROM workouts WHERE id = ?`, + [workoutId] + ); + + if (!workout) return null; + + // Get exercises for this workout + const exercises = await DatabaseService.db.getAllAsync(` + SELECT + we.id as workout_exercise_id, + we.order_index, + we.notes as exercise_notes, + e.id as exercise_id, + e.name, + e.muscle_group, + e.equipment_type + FROM workout_exercises we + JOIN exercises e ON we.exercise_id = e.id + WHERE we.workout_id = ? + ORDER BY we.order_index + `, [workoutId]); + + // Get sets for each exercise + for (const exercise of exercises) { + exercise.sets = await DatabaseService.db.getAllAsync(` + SELECT * FROM sets + WHERE workout_exercise_id = ? + ORDER BY set_number + `, [exercise.workout_exercise_id]); + } + + return { ...workout, exercises }; + } + + /** + * Get all workouts with summary data + */ + async getAllWorkouts({ limit = 50, offset = 0 } = {}) { + return await DatabaseService.db.getAllAsync(` + SELECT + w.*, + COUNT(DISTINCT we.id) as exercise_count, + SUM(s.weight_lbs * s.reps) as total_volume + FROM workouts w + LEFT JOIN workout_exercises we ON w.id = we.workout_id + LEFT JOIN sets s ON we.id = s.workout_exercise_id AND s.is_completed = 1 + GROUP BY w.id + ORDER BY w.date DESC, w.created_at DESC + LIMIT ? OFFSET ? + `, [limit, offset]); + } + + /** + * Get workouts by date range + */ + async getWorkoutsByDateRange(startDate, endDate) { + return await DatabaseService.db.getAllAsync(` + SELECT * FROM workouts + WHERE date BETWEEN ? AND ? + ORDER BY date DESC + `, [startDate, endDate]); + } + + /** + * Update workout end time and duration + */ + async finishWorkout(workoutId, endTime, durationSeconds) { + await DatabaseService.db.runAsync( + `UPDATE workouts SET end_time = ?, duration_seconds = ? WHERE id = ?`, + [endTime, durationSeconds, workoutId] + ); + } + + /** + * Update workout name + */ + async updateWorkoutName(workoutId, name) { + await DatabaseService.db.runAsync( + `UPDATE workouts SET name = ? WHERE id = ?`, + [name, workoutId] + ); + } + + /** + * Delete a workout (cascade deletes exercises and sets) + */ + async deleteWorkout(workoutId) { + await DatabaseService.db.runAsync( + `DELETE FROM workouts WHERE id = ?`, + [workoutId] + ); + } + + /** + * Get workout statistics + */ + async getWorkoutStats() { + const stats = await DatabaseService.db.getFirstAsync(` + SELECT + COUNT(*) as total_workouts, + SUM(duration_seconds) as total_duration, + AVG(duration_seconds) as avg_duration + FROM workouts + WHERE end_time IS NOT NULL + `); + + return stats; + } + + /** + * Calculate current workout streak + */ + async getCurrentStreak() { + const workouts = await DatabaseService.db.getAllAsync(` + SELECT DISTINCT date + FROM workouts + ORDER BY date DESC + LIMIT 365 + `); + + if (workouts.length === 0) return 0; + + let streak = 0; + let currentDate = new Date(); + currentDate.setHours(0, 0, 0, 0); + + for (const workout of workouts) { + const workoutDate = new Date(workout.date); + workoutDate.setHours(0, 0, 0, 0); + + const diffDays = Math.floor((currentDate - workoutDate) / (1000 * 60 * 60 * 24)); + + if (diffDays === streak || diffDays === streak + 1) { + streak = diffDays + 1; + } else { + break; + } + } + + return streak; + } +} + +export default new WorkoutRepository();