mirror of
https://github.com/DocNR/POWR.git
synced 2025-04-23 01:01:27 +00:00
updated new exercise template WIP
This commit is contained in:
parent
18d5886e40
commit
d9d8f238b1
52
CHANGELOG.md
52
CHANGELOG.md
@ -4,6 +4,58 @@ All notable changes to the POWR project will be documented in this file.
|
|||||||
|
|
||||||
## [Unreleased]
|
## [Unreleased]
|
||||||
|
|
||||||
|
# Changelog
|
||||||
|
|
||||||
|
All notable changes to the POWR project will be documented in this file.
|
||||||
|
|
||||||
|
## [Unreleased]
|
||||||
|
|
||||||
|
### 2024-02-05
|
||||||
|
#### Added
|
||||||
|
- Basic exercise template creation functionality
|
||||||
|
- Added input validation for required fields
|
||||||
|
- Implemented schema-compliant field constraints
|
||||||
|
- Added native picker components for standardized inputs
|
||||||
|
- Enhanced error handling in database operations
|
||||||
|
- Added detailed SQLite error logging
|
||||||
|
- Improved transaction management
|
||||||
|
- Added proper error types and propagation
|
||||||
|
|
||||||
|
#### Changed
|
||||||
|
- Updated NewExerciseScreen with constrained inputs
|
||||||
|
- Added dropdowns for equipment selection
|
||||||
|
- Added movement pattern selection
|
||||||
|
- Added difficulty selection
|
||||||
|
- Added exercise type selection
|
||||||
|
- Improved DbService with better error handling
|
||||||
|
- Added proper SQLite error types
|
||||||
|
- Enhanced transaction rollback handling
|
||||||
|
- Added detailed debug logging
|
||||||
|
|
||||||
|
#### Technical Details
|
||||||
|
1. Database Schema Enforcement:
|
||||||
|
- Added CHECK constraints for equipment types
|
||||||
|
- Added CHECK constraints for exercise types
|
||||||
|
- Added CHECK constraints for categories
|
||||||
|
- Proper handling of foreign key constraints
|
||||||
|
|
||||||
|
2. Input Validation:
|
||||||
|
- Equipment options: bodyweight, barbell, dumbbell, kettlebell, machine, cable, other
|
||||||
|
- Exercise types: strength, cardio, bodyweight
|
||||||
|
- Categories: Push, Pull, Legs, Core
|
||||||
|
- Difficulty levels: beginner, intermediate, advanced
|
||||||
|
- Movement patterns: push, pull, squat, hinge, carry, rotation
|
||||||
|
|
||||||
|
3. Error Handling:
|
||||||
|
- Added SQLite error type definitions
|
||||||
|
- Improved error propagation in LibraryService
|
||||||
|
- Added transaction rollback on constraint violations
|
||||||
|
|
||||||
|
#### Migration Notes
|
||||||
|
- Exercise creation now enforces schema constraints
|
||||||
|
- Input validation prevents invalid data entry
|
||||||
|
- Enhanced error messages provide better debugging information
|
||||||
|
|
||||||
### 2024-02-04
|
### 2024-02-04
|
||||||
|
|
||||||
#### Added
|
#### Added
|
||||||
|
@ -27,12 +27,12 @@ export default function TabLayout() {
|
|||||||
marginBottom: Platform.OS === 'ios' ? 0 : 4,
|
marginBottom: Platform.OS === 'ios' ? 0 : 4,
|
||||||
},
|
},
|
||||||
}}>
|
}}>
|
||||||
<Tabs.Screen
|
<Tabs.Screen
|
||||||
name="index"
|
name="profile"
|
||||||
options={{
|
options={{
|
||||||
title: 'Workout',
|
title: 'Profile',
|
||||||
tabBarIcon: ({ color, size }) => (
|
tabBarIcon: ({ color, size }) => (
|
||||||
<Dumbbell size={size} color={color} />
|
<User size={size} color={color} />
|
||||||
),
|
),
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
@ -45,6 +45,15 @@ export default function TabLayout() {
|
|||||||
),
|
),
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
<Tabs.Screen
|
||||||
|
name="index"
|
||||||
|
options={{
|
||||||
|
title: 'Workout',
|
||||||
|
tabBarIcon: ({ color, size }) => (
|
||||||
|
<Dumbbell size={size} color={color} />
|
||||||
|
),
|
||||||
|
}}
|
||||||
|
/>
|
||||||
<Tabs.Screen
|
<Tabs.Screen
|
||||||
name="social"
|
name="social"
|
||||||
options={{
|
options={{
|
||||||
@ -63,15 +72,6 @@ export default function TabLayout() {
|
|||||||
),
|
),
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
<Tabs.Screen
|
|
||||||
name="profile"
|
|
||||||
options={{
|
|
||||||
title: 'Profile',
|
|
||||||
tabBarIcon: ({ color, size }) => (
|
|
||||||
<User size={size} color={color} />
|
|
||||||
),
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</Tabs>
|
</Tabs>
|
||||||
);
|
);
|
||||||
}
|
}
|
@ -3,6 +3,7 @@ import React, { useState, useEffect, useRef, useCallback } from 'react';
|
|||||||
import { View, TouchableOpacity, StyleSheet, Platform } from 'react-native';
|
import { View, TouchableOpacity, StyleSheet, Platform } from 'react-native';
|
||||||
import { useColorScheme } from '@/hooks/useColorScheme';
|
import { useColorScheme } from '@/hooks/useColorScheme';
|
||||||
import { router, useLocalSearchParams } from 'expo-router';
|
import { router, useLocalSearchParams } from 'expo-router';
|
||||||
|
import { useFocusEffect } from '@react-navigation/native';
|
||||||
import { Plus } from 'lucide-react-native';
|
import { Plus } from 'lucide-react-native';
|
||||||
import { ThemedText } from '@/components/ThemedText';
|
import { ThemedText } from '@/components/ThemedText';
|
||||||
import TabLayout from '@/components/TabLayout';
|
import TabLayout from '@/components/TabLayout';
|
||||||
@ -71,9 +72,10 @@ export default function LibraryScreen() {
|
|||||||
const defaultSection = TABS.find(tab => tab.key === currentSection) ?? TABS[0];
|
const defaultSection = TABS.find(tab => tab.key === currentSection) ?? TABS[0];
|
||||||
const [activeSection, setActiveSection] = useState<number>(defaultSection.index);
|
const [activeSection, setActiveSection] = useState<number>(defaultSection.index);
|
||||||
|
|
||||||
// Load library content
|
// load library content
|
||||||
const loadContent = useCallback(async () => {
|
const loadContent = useCallback(async () => {
|
||||||
if (mounted) {
|
if (mounted) {
|
||||||
|
console.log('Starting content load');
|
||||||
setIsLoading(true);
|
setIsLoading(true);
|
||||||
try {
|
try {
|
||||||
const [exercises, templates] = await Promise.all([
|
const [exercises, templates] = await Promise.all([
|
||||||
@ -81,6 +83,9 @@ export default function LibraryScreen() {
|
|||||||
libraryService.getTemplates()
|
libraryService.getTemplates()
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
console.log('Loaded exercises:', exercises.length);
|
||||||
|
console.log('Loaded templates:', templates.length);
|
||||||
|
|
||||||
const exerciseContent: LibraryContent[] = exercises.map(exercise => ({
|
const exerciseContent: LibraryContent[] = exercises.map(exercise => ({
|
||||||
id: exercise.id,
|
id: exercise.id,
|
||||||
title: exercise.title,
|
title: exercise.title,
|
||||||
@ -96,7 +101,14 @@ export default function LibraryScreen() {
|
|||||||
}
|
}
|
||||||
}));
|
}));
|
||||||
|
|
||||||
setContent([...exerciseContent, ...templates]);
|
const newContent = [...exerciseContent, ...templates];
|
||||||
|
console.log('Setting new content:', newContent.length);
|
||||||
|
setContent(newContent);
|
||||||
|
|
||||||
|
// Force a re-filter of content
|
||||||
|
setFilteredContent(newContent.filter(item => {
|
||||||
|
// ... your existing filter logic ...
|
||||||
|
}));
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error loading library content:', error);
|
console.error('Error loading library content:', error);
|
||||||
} finally {
|
} finally {
|
||||||
@ -105,11 +117,28 @@ export default function LibraryScreen() {
|
|||||||
}
|
}
|
||||||
}, [mounted]);
|
}, [mounted]);
|
||||||
|
|
||||||
useEffect(() => {
|
useFocusEffect(
|
||||||
loadContent();
|
useCallback(() => {
|
||||||
}, [loadContent]);
|
console.log('Library screen focused, checking mount state:', mounted);
|
||||||
|
if (mounted) {
|
||||||
|
console.log('Loading content due to screen focus');
|
||||||
|
loadContent();
|
||||||
|
}
|
||||||
|
return () => {
|
||||||
|
console.log('Library screen unfocused');
|
||||||
|
};
|
||||||
|
}, [mounted, loadContent])
|
||||||
|
);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
console.log('Initial content load');
|
||||||
|
if (mounted) {
|
||||||
|
loadContent();
|
||||||
|
}
|
||||||
|
}, [mounted]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
console.log('Filtering content:', content.length);
|
||||||
const filtered = content.filter(item => {
|
const filtered = content.filter(item => {
|
||||||
if (searchQuery) {
|
if (searchQuery) {
|
||||||
const searchLower = searchQuery.toLowerCase();
|
const searchLower = searchQuery.toLowerCase();
|
||||||
@ -140,6 +169,7 @@ export default function LibraryScreen() {
|
|||||||
return true;
|
return true;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
console.log('Filtered content length:', filtered.length);
|
||||||
setFilteredContent(filtered);
|
setFilteredContent(filtered);
|
||||||
}, [content, searchQuery, filterOptions]);
|
}, [content, searchQuery, filterOptions]);
|
||||||
|
|
||||||
@ -176,7 +206,7 @@ export default function LibraryScreen() {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleAddContent = (type: 'exercise' | 'template') => {
|
const handleAddContent = useCallback((type: 'exercise' | 'template') => {
|
||||||
setShowAddContent(false);
|
setShowAddContent(false);
|
||||||
if (type === 'exercise') {
|
if (type === 'exercise') {
|
||||||
router.push('/(workout)/new-exercise' as const);
|
router.push('/(workout)/new-exercise' as const);
|
||||||
@ -185,7 +215,27 @@ export default function LibraryScreen() {
|
|||||||
pathname: '/(workout)/create-template' as const,
|
pathname: '/(workout)/create-template' as const,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
};
|
}, []);
|
||||||
|
|
||||||
|
// Then enhance the useFocusEffect to be more robust
|
||||||
|
useFocusEffect(
|
||||||
|
useCallback(() => {
|
||||||
|
console.log('Library screen focused, checking mount state:', mounted);
|
||||||
|
const loadIfMounted = async () => {
|
||||||
|
if (mounted) {
|
||||||
|
console.log('Loading content due to screen focus');
|
||||||
|
await loadContent();
|
||||||
|
console.log('Content load complete');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
loadIfMounted();
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
console.log('Library screen unfocused');
|
||||||
|
};
|
||||||
|
}, [mounted, loadContent])
|
||||||
|
);
|
||||||
|
|
||||||
const handleDeleteContent = useCallback((deletedContent: LibraryContent) => {
|
const handleDeleteContent = useCallback((deletedContent: LibraryContent) => {
|
||||||
setContent(prevContent =>
|
setContent(prevContent =>
|
||||||
@ -232,6 +282,19 @@ export default function LibraryScreen() {
|
|||||||
onChangeText={setSearchQuery}
|
onChangeText={setSearchQuery}
|
||||||
onFilterPress={() => setShowFilters(true)}
|
onFilterPress={() => setShowFilters(true)}
|
||||||
/>
|
/>
|
||||||
|
<TouchableOpacity
|
||||||
|
style={[styles.clearButton, { backgroundColor: colors.error }]}
|
||||||
|
onPress={async () => {
|
||||||
|
try {
|
||||||
|
await libraryService.clearDatabase();
|
||||||
|
await loadContent();
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error clearing database:', error);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<ThemedText style={{ color: '#FFFFFF' }}>Clear Database</ThemedText>
|
||||||
|
</TouchableOpacity>
|
||||||
</View>
|
</View>
|
||||||
|
|
||||||
<Pager
|
<Pager
|
||||||
@ -241,14 +304,14 @@ export default function LibraryScreen() {
|
|||||||
onPageSelected={handlePageSelected}
|
onPageSelected={handlePageSelected}
|
||||||
>
|
>
|
||||||
<View key="my-library" style={styles.pageContainer}>
|
<View key="my-library" style={styles.pageContainer}>
|
||||||
<MyLibrary
|
<MyLibrary
|
||||||
savedContent={filteredContent}
|
savedContent={filteredContent}
|
||||||
onContentPress={handleContentPress}
|
onContentPress={handleContentPress}
|
||||||
onFavoritePress={handleFavoritePress}
|
onFavoritePress={handleFavoritePress}
|
||||||
onDeleteContent={handleDeleteContent}
|
onDeleteContent={handleDeleteContent}
|
||||||
isLoading={isLoading}
|
isLoading={isLoading}
|
||||||
isVisible={activeSection === 0}
|
isVisible={activeSection === 0}
|
||||||
/>
|
/>
|
||||||
</View>
|
</View>
|
||||||
|
|
||||||
<View key="programs" style={styles.pageContainer}>
|
<View key="programs" style={styles.pageContainer}>
|
||||||
@ -354,4 +417,10 @@ const styles = StyleSheet.create({
|
|||||||
fontWeight: '500',
|
fontWeight: '500',
|
||||||
paddingHorizontal: spacing.small,
|
paddingHorizontal: spacing.small,
|
||||||
},
|
},
|
||||||
|
clearButton: {
|
||||||
|
marginTop: spacing.small,
|
||||||
|
padding: spacing.small,
|
||||||
|
borderRadius: 8,
|
||||||
|
alignItems: 'center',
|
||||||
|
},
|
||||||
});
|
});
|
@ -79,6 +79,13 @@ export default function NewExerciseScreen() {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Create unique tags array
|
||||||
|
const tags = Array.from(new Set([
|
||||||
|
difficulty,
|
||||||
|
movementPattern,
|
||||||
|
category.toLowerCase()
|
||||||
|
]));
|
||||||
|
|
||||||
const exerciseTemplate = {
|
const exerciseTemplate = {
|
||||||
title: title.trim(),
|
title: title.trim(),
|
||||||
type: exerciseType,
|
type: exerciseType,
|
||||||
@ -86,11 +93,7 @@ export default function NewExerciseScreen() {
|
|||||||
equipment,
|
equipment,
|
||||||
difficulty,
|
difficulty,
|
||||||
description: instructions.trim(),
|
description: instructions.trim(),
|
||||||
tags: [
|
tags, // Now using deduplicated tags
|
||||||
difficulty,
|
|
||||||
movementPattern,
|
|
||||||
category.toLowerCase()
|
|
||||||
],
|
|
||||||
format: {
|
format: {
|
||||||
weight: true,
|
weight: true,
|
||||||
reps: true,
|
reps: true,
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
// components/library/MyLibrary.tsx
|
// components/library/MyLibrary.tsx
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { View, FlatList, StyleSheet, Platform } from 'react-native';
|
import { View, FlatList, StyleSheet, Platform, ActivityIndicator } from 'react-native';
|
||||||
import { Feather } from '@expo/vector-icons';
|
import { Feather } from '@expo/vector-icons';
|
||||||
import { useColorScheme } from '@/hooks/useColorScheme';
|
import { useColorScheme } from '@/hooks/useColorScheme';
|
||||||
import { LibraryContent } from '@/types/exercise';
|
import { LibraryContent } from '@/types/exercise';
|
||||||
@ -23,15 +23,32 @@ export default function MyLibrary({
|
|||||||
onContentPress,
|
onContentPress,
|
||||||
onFavoritePress,
|
onFavoritePress,
|
||||||
onDeleteContent,
|
onDeleteContent,
|
||||||
|
isLoading = false,
|
||||||
isVisible = true
|
isVisible = true
|
||||||
}: MyLibraryProps) {
|
}: MyLibraryProps) {
|
||||||
const { colors } = useColorScheme();
|
const { colors } = useColorScheme();
|
||||||
|
|
||||||
|
console.log('MyLibrary render:', {
|
||||||
|
contentLength: savedContent.length,
|
||||||
|
isVisible,
|
||||||
|
isLoading
|
||||||
|
});
|
||||||
|
|
||||||
// Don't render anything if not visible
|
// Don't render anything if not visible
|
||||||
if (!isVisible) {
|
if (!isVisible) {
|
||||||
|
console.log('MyLibrary not visible, returning null');
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Show loading state
|
||||||
|
if (isLoading) {
|
||||||
|
return (
|
||||||
|
<View style={[styles.loadingContainer, { backgroundColor: colors.background }]}>
|
||||||
|
<ActivityIndicator size="large" color={colors.primary} />
|
||||||
|
</View>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
const handleDelete = async (content: LibraryContent) => {
|
const handleDelete = async (content: LibraryContent) => {
|
||||||
try {
|
try {
|
||||||
if (content.type === 'exercise') {
|
if (content.type === 'exercise') {
|
||||||
@ -48,6 +65,12 @@ export default function MyLibrary({
|
|||||||
const exercises = savedContent.filter(content => content.type === 'exercise');
|
const exercises = savedContent.filter(content => content.type === 'exercise');
|
||||||
const workouts = savedContent.filter(content => content.type === 'workout');
|
const workouts = savedContent.filter(content => content.type === 'workout');
|
||||||
|
|
||||||
|
console.log('Content breakdown:', {
|
||||||
|
total: savedContent.length,
|
||||||
|
exercises: exercises.length,
|
||||||
|
workouts: workouts.length
|
||||||
|
});
|
||||||
|
|
||||||
const renderSection = (title: string, items: LibraryContent[]) => {
|
const renderSection = (title: string, items: LibraryContent[]) => {
|
||||||
if (items.length === 0) return null;
|
if (items.length === 0) return null;
|
||||||
|
|
||||||
@ -66,7 +89,7 @@ export default function MyLibrary({
|
|||||||
onDelete={item.type === 'exercise' ? () => handleDelete(item) : undefined}
|
onDelete={item.type === 'exercise' ? () => handleDelete(item) : undefined}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
keyExtractor={item => item.id}
|
keyExtractor={(item, index) => `${title}-${item.id}-${index}`}
|
||||||
scrollEnabled={false}
|
scrollEnabled={false}
|
||||||
/>
|
/>
|
||||||
</View>
|
</View>
|
||||||
@ -129,4 +152,9 @@ const styles = StyleSheet.create({
|
|||||||
textAlign: 'center',
|
textAlign: 'center',
|
||||||
maxWidth: '80%',
|
maxWidth: '80%',
|
||||||
},
|
},
|
||||||
|
loadingContainer: {
|
||||||
|
flex: 1,
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'center',
|
||||||
|
},
|
||||||
});
|
});
|
@ -10,6 +10,7 @@ import {
|
|||||||
import { WorkoutTemplate } from '@/types/workout';
|
import { WorkoutTemplate } from '@/types/workout';
|
||||||
import { StorageSource } from '@/types/shared';
|
import { StorageSource } from '@/types/shared';
|
||||||
import { SQLiteError } from '@/types/sqlite';
|
import { SQLiteError } from '@/types/sqlite';
|
||||||
|
import { schema } from '@/utils/db/schema';
|
||||||
|
|
||||||
class LibraryService {
|
class LibraryService {
|
||||||
private db: DbService;
|
private db: DbService;
|
||||||
@ -23,11 +24,14 @@ class LibraryService {
|
|||||||
const id = generateId();
|
const id = generateId();
|
||||||
const timestamp = Date.now();
|
const timestamp = Date.now();
|
||||||
|
|
||||||
|
// Deduplicate tags
|
||||||
|
const uniqueTags = Array.from(new Set(exercise.tags));
|
||||||
|
|
||||||
if (this.DEBUG) {
|
if (this.DEBUG) {
|
||||||
console.log('Creating exercise with payload:', {
|
console.log('Creating exercise with payload:', {
|
||||||
id,
|
id,
|
||||||
timestamp,
|
timestamp,
|
||||||
exercise,
|
exercise: { ...exercise, tags: uniqueTags },
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -79,8 +83,8 @@ class LibraryService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 3. Insert tags if provided
|
// 3. Insert tags if provided
|
||||||
if (exercise.tags?.length) {
|
if (uniqueTags?.length) {
|
||||||
for (const tag of exercise.tags) {
|
for (const tag of uniqueTags) {
|
||||||
const tagResult = await this.db.executeWrite(
|
const tagResult = await this.db.executeWrite(
|
||||||
`INSERT INTO exercise_tags (exercise_id, tag) VALUES (?, ?)`,
|
`INSERT INTO exercise_tags (exercise_id, tag) VALUES (?, ?)`,
|
||||||
[id, tag]
|
[id, tag]
|
||||||
@ -220,6 +224,14 @@ class LibraryService {
|
|||||||
|
|
||||||
const result = await this.db.executeSql(query, category ? [category] : []);
|
const result = await this.db.executeSql(query, category ? [category] : []);
|
||||||
|
|
||||||
|
// Add this logging
|
||||||
|
console.log(`Found ${result.rows.length} exercises in database:`,
|
||||||
|
Array.from({ length: result.rows.length }, (_, i) => ({
|
||||||
|
id: result.rows.item(i).id,
|
||||||
|
title: result.rows.item(i).title
|
||||||
|
}))
|
||||||
|
);
|
||||||
|
|
||||||
return Array.from({ length: result.rows.length }, (_, i) => {
|
return Array.from({ length: result.rows.length }, (_, i) => {
|
||||||
const row = result.rows.item(i);
|
const row = result.rows.item(i);
|
||||||
return {
|
return {
|
||||||
@ -295,7 +307,15 @@ class LibraryService {
|
|||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
return [...exerciseContent, ...templateContent];
|
const allContent = [...exerciseContent, ...templateContent];
|
||||||
|
|
||||||
|
// Ensure no duplicates by ID
|
||||||
|
const uniqueContent = Array.from(
|
||||||
|
new Map(allContent.map(item => [item.id, item])).values()
|
||||||
|
);
|
||||||
|
|
||||||
|
return uniqueContent;
|
||||||
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error getting templates:', error);
|
console.error('Error getting templates:', error);
|
||||||
throw error;
|
throw error;
|
||||||
@ -349,6 +369,49 @@ class LibraryService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async clearDatabase(): Promise<void> {
|
||||||
|
if (this.DEBUG) {
|
||||||
|
console.log('Clearing database...');
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Disable foreign key constraints
|
||||||
|
await this.db.executeWrite('PRAGMA foreign_keys = OFF;');
|
||||||
|
|
||||||
|
// Drop all tables
|
||||||
|
const dropQueries = [
|
||||||
|
'DROP TABLE IF EXISTS template_tags',
|
||||||
|
'DROP TABLE IF EXISTS template_exercises',
|
||||||
|
'DROP TABLE IF EXISTS exercise_tags',
|
||||||
|
'DROP TABLE IF EXISTS exercise_instructions',
|
||||||
|
'DROP TABLE IF EXISTS exercise_format',
|
||||||
|
'DROP TABLE IF EXISTS templates',
|
||||||
|
'DROP TABLE IF EXISTS exercises',
|
||||||
|
'DROP TABLE IF EXISTS schema_version'
|
||||||
|
];
|
||||||
|
|
||||||
|
for (const query of dropQueries) {
|
||||||
|
if (this.DEBUG) {
|
||||||
|
console.log('Executing:', query);
|
||||||
|
}
|
||||||
|
await this.db.executeWrite(query);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Re-enable foreign key constraints
|
||||||
|
await this.db.executeWrite('PRAGMA foreign_keys = ON;');
|
||||||
|
|
||||||
|
// Recreate schema using the schema utility
|
||||||
|
await schema.migrate();
|
||||||
|
|
||||||
|
if (this.DEBUG) {
|
||||||
|
console.log('Database cleared and reinitialized successfully');
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error clearing database:', error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private async getTemplateExercises(templateId: string): Promise<Array<{
|
private async getTemplateExercises(templateId: string): Promise<Array<{
|
||||||
exercise: BaseExercise;
|
exercise: BaseExercise;
|
||||||
targetSets: number;
|
targetSets: number;
|
||||||
|
@ -117,77 +117,108 @@ class Schema {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async migrate(): Promise<void> {
|
async migrate(): Promise<void> {
|
||||||
const currentVersion = await this.getCurrentVersion();
|
try {
|
||||||
|
const currentVersion = await this.getCurrentVersion();
|
||||||
|
|
||||||
if (currentVersion < SCHEMA_VERSION) {
|
console.log('Current database version:', currentVersion);
|
||||||
if (currentVersion < 1) {
|
console.log('Target database version:', SCHEMA_VERSION);
|
||||||
await this.createTables();
|
|
||||||
await this.setVersion(1);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Migration to version 2 - Add format table
|
if (currentVersion < SCHEMA_VERSION) {
|
||||||
if (currentVersion < 2) {
|
console.log('Starting database migration...');
|
||||||
await this.db.executeWrite(`
|
|
||||||
CREATE TABLE IF NOT EXISTS exercise_format (
|
|
||||||
exercise_id TEXT PRIMARY KEY,
|
|
||||||
format_json TEXT NOT NULL,
|
|
||||||
units_json TEXT,
|
|
||||||
FOREIGN KEY(exercise_id) REFERENCES exercises(id) ON DELETE CASCADE
|
|
||||||
);
|
|
||||||
`);
|
|
||||||
await this.setVersion(2);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Migration to version 3 - Add template tables
|
// Initial migration
|
||||||
if (currentVersion < 3) {
|
if (currentVersion < 1) {
|
||||||
await this.db.executeWriteMany([
|
console.log('Running migration to version 1...');
|
||||||
{
|
await this.createTables();
|
||||||
sql: `CREATE TABLE IF NOT EXISTS templates (
|
await this.setVersion(1);
|
||||||
id TEXT PRIMARY KEY,
|
}
|
||||||
title TEXT NOT NULL,
|
|
||||||
type TEXT NOT NULL CHECK(type IN ('strength', 'circuit', 'emom', 'amrap')),
|
// Migration to version 2 - Add format table
|
||||||
category TEXT NOT NULL CHECK(category IN ('Full Body', 'Custom', 'Push/Pull/Legs', 'Upper/Lower', 'Cardio', 'CrossFit', 'Strength')),
|
if (currentVersion < 2) {
|
||||||
description TEXT,
|
console.log('Running migration to version 2...');
|
||||||
author_name TEXT,
|
try {
|
||||||
author_pubkey TEXT,
|
await this.db.executeWrite(`
|
||||||
rounds INTEGER,
|
CREATE TABLE IF NOT EXISTS exercise_format (
|
||||||
duration INTEGER,
|
exercise_id TEXT PRIMARY KEY,
|
||||||
interval_time INTEGER,
|
format_json TEXT NOT NULL,
|
||||||
rest_between_rounds INTEGER,
|
units_json TEXT,
|
||||||
is_public BOOLEAN NOT NULL DEFAULT 0,
|
FOREIGN KEY(exercise_id) REFERENCES exercises(id) ON DELETE CASCADE
|
||||||
created_at INTEGER NOT NULL,
|
);
|
||||||
updated_at INTEGER NOT NULL,
|
`);
|
||||||
metadata_json TEXT,
|
await this.setVersion(2);
|
||||||
availability_json TEXT NOT NULL,
|
console.log('Migration to version 2 completed');
|
||||||
source TEXT NOT NULL DEFAULT 'local'
|
} catch (error) {
|
||||||
);`
|
console.error('Error in version 2 migration:', error);
|
||||||
},
|
throw error;
|
||||||
{
|
|
||||||
sql: `CREATE TABLE IF NOT EXISTS template_exercises (
|
|
||||||
template_id TEXT NOT NULL,
|
|
||||||
exercise_id TEXT NOT NULL,
|
|
||||||
target_sets INTEGER,
|
|
||||||
target_reps INTEGER,
|
|
||||||
target_weight REAL,
|
|
||||||
target_rpe INTEGER CHECK(target_rpe BETWEEN 0 AND 10),
|
|
||||||
notes TEXT,
|
|
||||||
display_order INTEGER NOT NULL,
|
|
||||||
FOREIGN KEY(template_id) REFERENCES templates(id) ON DELETE CASCADE,
|
|
||||||
FOREIGN KEY(exercise_id) REFERENCES exercises(id) ON DELETE CASCADE,
|
|
||||||
PRIMARY KEY(template_id, exercise_id, display_order)
|
|
||||||
);`
|
|
||||||
},
|
|
||||||
{
|
|
||||||
sql: `CREATE TABLE IF NOT EXISTS template_tags (
|
|
||||||
template_id TEXT NOT NULL,
|
|
||||||
tag TEXT NOT NULL,
|
|
||||||
FOREIGN KEY(template_id) REFERENCES templates(id) ON DELETE CASCADE,
|
|
||||||
UNIQUE(template_id, tag)
|
|
||||||
);`
|
|
||||||
}
|
}
|
||||||
]);
|
}
|
||||||
await this.setVersion(3);
|
|
||||||
|
// Migration to version 3 - Add template tables
|
||||||
|
if (currentVersion < 3) {
|
||||||
|
console.log('Running migration to version 3...');
|
||||||
|
try {
|
||||||
|
await this.db.executeWriteMany([
|
||||||
|
{
|
||||||
|
sql: `CREATE TABLE IF NOT EXISTS templates (
|
||||||
|
id TEXT PRIMARY KEY,
|
||||||
|
title TEXT NOT NULL,
|
||||||
|
type TEXT NOT NULL CHECK(type IN ('strength', 'circuit', 'emom', 'amrap')),
|
||||||
|
category TEXT NOT NULL CHECK(category IN ('Full Body', 'Custom', 'Push/Pull/Legs', 'Upper/Lower', 'Cardio', 'CrossFit', 'Strength')),
|
||||||
|
description TEXT,
|
||||||
|
notes TEXT,
|
||||||
|
author_name TEXT,
|
||||||
|
author_pubkey TEXT,
|
||||||
|
rounds INTEGER,
|
||||||
|
duration INTEGER,
|
||||||
|
interval_time INTEGER,
|
||||||
|
rest_between_rounds INTEGER,
|
||||||
|
is_public BOOLEAN NOT NULL DEFAULT 0,
|
||||||
|
created_at INTEGER NOT NULL,
|
||||||
|
updated_at INTEGER NOT NULL,
|
||||||
|
metadata_json TEXT,
|
||||||
|
availability_json TEXT NOT NULL,
|
||||||
|
source TEXT NOT NULL DEFAULT 'local'
|
||||||
|
)`
|
||||||
|
},
|
||||||
|
{
|
||||||
|
sql: `CREATE TABLE IF NOT EXISTS template_exercises (
|
||||||
|
template_id TEXT NOT NULL,
|
||||||
|
exercise_id TEXT NOT NULL,
|
||||||
|
target_sets INTEGER,
|
||||||
|
target_reps INTEGER,
|
||||||
|
target_weight REAL,
|
||||||
|
target_rpe INTEGER CHECK(target_rpe BETWEEN 0 AND 10),
|
||||||
|
notes TEXT,
|
||||||
|
display_order INTEGER NOT NULL,
|
||||||
|
FOREIGN KEY(template_id) REFERENCES templates(id) ON DELETE CASCADE,
|
||||||
|
FOREIGN KEY(exercise_id) REFERENCES exercises(id) ON DELETE CASCADE,
|
||||||
|
PRIMARY KEY(template_id, exercise_id, display_order)
|
||||||
|
)`
|
||||||
|
},
|
||||||
|
{
|
||||||
|
sql: `CREATE TABLE IF NOT EXISTS template_tags (
|
||||||
|
template_id TEXT NOT NULL,
|
||||||
|
tag TEXT NOT NULL,
|
||||||
|
FOREIGN KEY(template_id) REFERENCES templates(id) ON DELETE CASCADE,
|
||||||
|
UNIQUE(template_id, tag)
|
||||||
|
)`
|
||||||
|
}
|
||||||
|
]);
|
||||||
|
await this.setVersion(3);
|
||||||
|
console.log('Migration to version 3 completed');
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error in version 3 migration:', error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('All migrations completed successfully');
|
||||||
|
} else {
|
||||||
|
console.log('Database is up to date');
|
||||||
}
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Migration failed:', error);
|
||||||
|
throw error;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Loading…
x
Reference in New Issue
Block a user