mirror of
https://github.com/DocNR/POWR.git
synced 2025-04-19 10:51:19 +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]
|
||||
|
||||
# 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
|
||||
|
||||
#### Added
|
||||
|
@ -27,12 +27,12 @@ export default function TabLayout() {
|
||||
marginBottom: Platform.OS === 'ios' ? 0 : 4,
|
||||
},
|
||||
}}>
|
||||
<Tabs.Screen
|
||||
name="index"
|
||||
<Tabs.Screen
|
||||
name="profile"
|
||||
options={{
|
||||
title: 'Workout',
|
||||
title: 'Profile',
|
||||
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
|
||||
name="social"
|
||||
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>
|
||||
);
|
||||
);
|
||||
}
|
@ -3,6 +3,7 @@ import React, { useState, useEffect, useRef, useCallback } from 'react';
|
||||
import { View, TouchableOpacity, StyleSheet, Platform } from 'react-native';
|
||||
import { useColorScheme } from '@/hooks/useColorScheme';
|
||||
import { router, useLocalSearchParams } from 'expo-router';
|
||||
import { useFocusEffect } from '@react-navigation/native';
|
||||
import { Plus } from 'lucide-react-native';
|
||||
import { ThemedText } from '@/components/ThemedText';
|
||||
import TabLayout from '@/components/TabLayout';
|
||||
@ -71,16 +72,20 @@ export default function LibraryScreen() {
|
||||
const defaultSection = TABS.find(tab => tab.key === currentSection) ?? TABS[0];
|
||||
const [activeSection, setActiveSection] = useState<number>(defaultSection.index);
|
||||
|
||||
// Load library content
|
||||
// load library content
|
||||
const loadContent = useCallback(async () => {
|
||||
if (mounted) {
|
||||
console.log('Starting content load');
|
||||
setIsLoading(true);
|
||||
try {
|
||||
const [exercises, templates] = await Promise.all([
|
||||
libraryService.getExercises(),
|
||||
libraryService.getTemplates()
|
||||
]);
|
||||
|
||||
|
||||
console.log('Loaded exercises:', exercises.length);
|
||||
console.log('Loaded templates:', templates.length);
|
||||
|
||||
const exerciseContent: LibraryContent[] = exercises.map(exercise => ({
|
||||
id: exercise.id,
|
||||
title: exercise.title,
|
||||
@ -95,8 +100,15 @@ export default function LibraryScreen() {
|
||||
source: ['local']
|
||||
}
|
||||
}));
|
||||
|
||||
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) {
|
||||
console.error('Error loading library content:', error);
|
||||
} finally {
|
||||
@ -105,11 +117,28 @@ export default function LibraryScreen() {
|
||||
}
|
||||
}, [mounted]);
|
||||
|
||||
useEffect(() => {
|
||||
loadContent();
|
||||
}, [loadContent]);
|
||||
useFocusEffect(
|
||||
useCallback(() => {
|
||||
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(() => {
|
||||
console.log('Initial content load');
|
||||
if (mounted) {
|
||||
loadContent();
|
||||
}
|
||||
}, [mounted]);
|
||||
|
||||
useEffect(() => {
|
||||
console.log('Filtering content:', content.length);
|
||||
const filtered = content.filter(item => {
|
||||
if (searchQuery) {
|
||||
const searchLower = searchQuery.toLowerCase();
|
||||
@ -140,6 +169,7 @@ export default function LibraryScreen() {
|
||||
return true;
|
||||
});
|
||||
|
||||
console.log('Filtered content length:', filtered.length);
|
||||
setFilteredContent(filtered);
|
||||
}, [content, searchQuery, filterOptions]);
|
||||
|
||||
@ -176,7 +206,7 @@ export default function LibraryScreen() {
|
||||
}
|
||||
};
|
||||
|
||||
const handleAddContent = (type: 'exercise' | 'template') => {
|
||||
const handleAddContent = useCallback((type: 'exercise' | 'template') => {
|
||||
setShowAddContent(false);
|
||||
if (type === 'exercise') {
|
||||
router.push('/(workout)/new-exercise' as const);
|
||||
@ -185,7 +215,27 @@ export default function LibraryScreen() {
|
||||
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) => {
|
||||
setContent(prevContent =>
|
||||
@ -232,6 +282,19 @@ export default function LibraryScreen() {
|
||||
onChangeText={setSearchQuery}
|
||||
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>
|
||||
|
||||
<Pager
|
||||
@ -241,14 +304,14 @@ export default function LibraryScreen() {
|
||||
onPageSelected={handlePageSelected}
|
||||
>
|
||||
<View key="my-library" style={styles.pageContainer}>
|
||||
<MyLibrary
|
||||
savedContent={filteredContent}
|
||||
onContentPress={handleContentPress}
|
||||
onFavoritePress={handleFavoritePress}
|
||||
onDeleteContent={handleDeleteContent}
|
||||
isLoading={isLoading}
|
||||
isVisible={activeSection === 0}
|
||||
/>
|
||||
<MyLibrary
|
||||
savedContent={filteredContent}
|
||||
onContentPress={handleContentPress}
|
||||
onFavoritePress={handleFavoritePress}
|
||||
onDeleteContent={handleDeleteContent}
|
||||
isLoading={isLoading}
|
||||
isVisible={activeSection === 0}
|
||||
/>
|
||||
</View>
|
||||
|
||||
<View key="programs" style={styles.pageContainer}>
|
||||
@ -354,4 +417,10 @@ const styles = StyleSheet.create({
|
||||
fontWeight: '500',
|
||||
paddingHorizontal: spacing.small,
|
||||
},
|
||||
clearButton: {
|
||||
marginTop: spacing.small,
|
||||
padding: spacing.small,
|
||||
borderRadius: 8,
|
||||
alignItems: 'center',
|
||||
},
|
||||
});
|
@ -73,12 +73,19 @@ export default function NewExerciseScreen() {
|
||||
try {
|
||||
setError(null);
|
||||
setIsSubmitting(true);
|
||||
|
||||
|
||||
if (!title.trim()) {
|
||||
setError('Exercise name is required');
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
// Create unique tags array
|
||||
const tags = Array.from(new Set([
|
||||
difficulty,
|
||||
movementPattern,
|
||||
category.toLowerCase()
|
||||
]));
|
||||
|
||||
const exerciseTemplate = {
|
||||
title: title.trim(),
|
||||
type: exerciseType,
|
||||
@ -86,11 +93,7 @@ export default function NewExerciseScreen() {
|
||||
equipment,
|
||||
difficulty,
|
||||
description: instructions.trim(),
|
||||
tags: [
|
||||
difficulty,
|
||||
movementPattern,
|
||||
category.toLowerCase()
|
||||
],
|
||||
tags, // Now using deduplicated tags
|
||||
format: {
|
||||
weight: true,
|
||||
reps: true,
|
||||
|
@ -1,6 +1,6 @@
|
||||
// components/library/MyLibrary.tsx
|
||||
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 { useColorScheme } from '@/hooks/useColorScheme';
|
||||
import { LibraryContent } from '@/types/exercise';
|
||||
@ -23,15 +23,32 @@ export default function MyLibrary({
|
||||
onContentPress,
|
||||
onFavoritePress,
|
||||
onDeleteContent,
|
||||
isLoading = false,
|
||||
isVisible = true
|
||||
}: MyLibraryProps) {
|
||||
const { colors } = useColorScheme();
|
||||
|
||||
console.log('MyLibrary render:', {
|
||||
contentLength: savedContent.length,
|
||||
isVisible,
|
||||
isLoading
|
||||
});
|
||||
|
||||
// Don't render anything if not visible
|
||||
if (!isVisible) {
|
||||
console.log('MyLibrary not visible, returning 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) => {
|
||||
try {
|
||||
if (content.type === 'exercise') {
|
||||
@ -48,6 +65,12 @@ export default function MyLibrary({
|
||||
const exercises = savedContent.filter(content => content.type === 'exercise');
|
||||
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[]) => {
|
||||
if (items.length === 0) return null;
|
||||
|
||||
@ -66,7 +89,7 @@ export default function MyLibrary({
|
||||
onDelete={item.type === 'exercise' ? () => handleDelete(item) : undefined}
|
||||
/>
|
||||
)}
|
||||
keyExtractor={item => item.id}
|
||||
keyExtractor={(item, index) => `${title}-${item.id}-${index}`}
|
||||
scrollEnabled={false}
|
||||
/>
|
||||
</View>
|
||||
@ -129,4 +152,9 @@ const styles = StyleSheet.create({
|
||||
textAlign: 'center',
|
||||
maxWidth: '80%',
|
||||
},
|
||||
loadingContainer: {
|
||||
flex: 1,
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
},
|
||||
});
|
@ -10,6 +10,7 @@ import {
|
||||
import { WorkoutTemplate } from '@/types/workout';
|
||||
import { StorageSource } from '@/types/shared';
|
||||
import { SQLiteError } from '@/types/sqlite';
|
||||
import { schema } from '@/utils/db/schema';
|
||||
|
||||
class LibraryService {
|
||||
private db: DbService;
|
||||
@ -22,15 +23,18 @@ class LibraryService {
|
||||
async addExercise(exercise: Omit<BaseExercise, 'id' | 'created_at' | 'availability'>): Promise<string> {
|
||||
const id = generateId();
|
||||
const timestamp = Date.now();
|
||||
|
||||
|
||||
// Deduplicate tags
|
||||
const uniqueTags = Array.from(new Set(exercise.tags));
|
||||
|
||||
if (this.DEBUG) {
|
||||
console.log('Creating exercise with payload:', {
|
||||
id,
|
||||
timestamp,
|
||||
exercise,
|
||||
exercise: { ...exercise, tags: uniqueTags },
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
try {
|
||||
await this.db.withTransaction(async () => {
|
||||
// 1. First insert main exercise data
|
||||
@ -79,19 +83,19 @@ class LibraryService {
|
||||
}
|
||||
|
||||
// 3. Insert tags if provided
|
||||
if (exercise.tags?.length) {
|
||||
for (const tag of exercise.tags) {
|
||||
if (uniqueTags?.length) {
|
||||
for (const tag of uniqueTags) {
|
||||
const tagResult = await this.db.executeWrite(
|
||||
`INSERT INTO exercise_tags (exercise_id, tag) VALUES (?, ?)`,
|
||||
[id, tag]
|
||||
);
|
||||
|
||||
|
||||
if (this.DEBUG) {
|
||||
console.log('Tag insert result:', tagResult);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// 4. Insert instructions if provided
|
||||
if (exercise.instructions?.length) {
|
||||
for (const [index, instruction] of exercise.instructions.entries()) {
|
||||
@ -217,8 +221,16 @@ class LibraryService {
|
||||
GROUP BY e.id
|
||||
ORDER BY e.title
|
||||
`;
|
||||
|
||||
|
||||
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) => {
|
||||
const row = result.rows.item(i);
|
||||
@ -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) {
|
||||
console.error('Error getting templates:', 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<{
|
||||
exercise: BaseExercise;
|
||||
targetSets: number;
|
||||
|
@ -117,77 +117,108 @@ class Schema {
|
||||
}
|
||||
|
||||
async migrate(): Promise<void> {
|
||||
const currentVersion = await this.getCurrentVersion();
|
||||
|
||||
if (currentVersion < SCHEMA_VERSION) {
|
||||
if (currentVersion < 1) {
|
||||
await this.createTables();
|
||||
await this.setVersion(1);
|
||||
}
|
||||
try {
|
||||
const currentVersion = await this.getCurrentVersion();
|
||||
|
||||
// Migration to version 2 - Add format table
|
||||
if (currentVersion < 2) {
|
||||
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
|
||||
if (currentVersion < 3) {
|
||||
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,
|
||||
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)
|
||||
);`
|
||||
console.log('Current database version:', currentVersion);
|
||||
console.log('Target database version:', SCHEMA_VERSION);
|
||||
|
||||
if (currentVersion < SCHEMA_VERSION) {
|
||||
console.log('Starting database migration...');
|
||||
|
||||
// Initial migration
|
||||
if (currentVersion < 1) {
|
||||
console.log('Running migration to version 1...');
|
||||
await this.createTables();
|
||||
await this.setVersion(1);
|
||||
}
|
||||
|
||||
// Migration to version 2 - Add format table
|
||||
if (currentVersion < 2) {
|
||||
console.log('Running migration to version 2...');
|
||||
try {
|
||||
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);
|
||||
console.log('Migration to version 2 completed');
|
||||
} catch (error) {
|
||||
console.error('Error in version 2 migration:', error);
|
||||
throw error;
|
||||
}
|
||||
]);
|
||||
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