mirror of
https://github.com/DocNR/POWR.git
synced 2025-06-05 08:42:05 +00:00
bug fix to associate exercise and workouts in powr pack
This commit is contained in:
parent
a3e9dc36d8
commit
5b706b3894
@ -232,7 +232,6 @@ export default function ExercisesScreen() {
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
);
|
||||
|
||||
{/* Filter Sheet */}
|
||||
<FilterSheet
|
||||
|
@ -1,5 +1,5 @@
|
||||
import React from 'react';
|
||||
import { View, ActivityIndicator, Text } from 'react-native';
|
||||
import { View, ActivityIndicator, ScrollView, Text } from 'react-native';
|
||||
import { SQLiteProvider, openDatabaseSync, SQLiteDatabase } from 'expo-sqlite';
|
||||
import { schema } from '@/lib/db/schema';
|
||||
import { ExerciseService } from '@/lib/db/services/ExerciseService';
|
||||
@ -64,7 +64,6 @@ const DelayedInitializer: React.FC<{children: React.ReactNode}> = ({children}) =
|
||||
|
||||
return <>{children}</>;
|
||||
};
|
||||
|
||||
export function DatabaseProvider({ children }: DatabaseProviderProps) {
|
||||
const [isReady, setIsReady] = React.useState(false);
|
||||
const [error, setError] = React.useState<string | null>(null);
|
||||
@ -107,11 +106,29 @@ export function DatabaseProvider({ children }: DatabaseProviderProps) {
|
||||
// Explicitly check for critical tables after schema creation
|
||||
await schema.ensureCriticalTablesExist(db);
|
||||
|
||||
// Run the v8 migration explicitly to ensure new columns are added
|
||||
try {
|
||||
await (schema as any).migrate_v8(db);
|
||||
console.log('[DB] Migration v8 executed successfully');
|
||||
} catch (migrationError) {
|
||||
console.warn('[DB] Error running migration v8:', migrationError);
|
||||
// Continue even if migration fails - tables might already be updated
|
||||
}
|
||||
|
||||
// Run v9 migration for Nostr metadata enhancements
|
||||
try {
|
||||
await (schema as any).migrate_v9(db);
|
||||
console.log('[DB] Migration v9 executed successfully');
|
||||
} catch (migrationError) {
|
||||
console.warn('[DB] Error running migration v9:', migrationError);
|
||||
// Continue even if migration fails - tables might already be updated
|
||||
}
|
||||
|
||||
// Initialize services
|
||||
console.log('[DB] Initializing services...');
|
||||
const exerciseService = new ExerciseService(db);
|
||||
const workoutService = new WorkoutService(db);
|
||||
const templateService = new TemplateService(db);
|
||||
const templateService = new TemplateService(db, exerciseService);
|
||||
const devSeeder = new DevSeederService(db, exerciseService);
|
||||
const publicationQueue = new PublicationQueueService(db);
|
||||
const favoritesService = new FavoritesService(db);
|
||||
@ -137,7 +154,6 @@ export function DatabaseProvider({ children }: DatabaseProviderProps) {
|
||||
powrPackService,
|
||||
db,
|
||||
});
|
||||
|
||||
// Seed development database
|
||||
if (__DEV__) {
|
||||
console.log('[DB] Seeding development database...');
|
||||
|
@ -12,46 +12,102 @@ import { Skeleton } from '@/components/ui/skeleton';
|
||||
import { PackageOpen, ArrowRight } from 'lucide-react-native';
|
||||
import { NDKEvent } from '@nostr-dev-kit/ndk-mobile';
|
||||
import { usePOWRPackService } from '@/components/DatabaseProvider';
|
||||
import { Clipboard } from 'react-native';
|
||||
|
||||
// Hardcoded test pack naddr
|
||||
const TEST_PACK_NADDR = 'naddr1qq88qmmhwgkhgetnwskhqctrdvqs6amnwvaz7tmwdaejumr0dsq3gamnwvaz7tmjv4kxz7fwv3sk6atn9e5k7q3q25f8lj0pcq7xk3v68w4h9ldenhh3v3x97gumm5yl8e0mgq0dnvssxpqqqp6ng325rsl';
|
||||
|
||||
export default function POWRPackSection() {
|
||||
const { ndk } = useNDK();
|
||||
const powrPackService = usePOWRPackService();
|
||||
const [featuredPacks, setFeaturedPacks] = useState<NDKEvent[]>([]);
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
|
||||
// Subscribe to POWR packs (kind 30004 with powrpack hashtag)
|
||||
const { events, isLoading } = useSubscribe(
|
||||
const { events, isLoading: isSubscribeLoading } = useSubscribe(
|
||||
ndk ? [{ kinds: [30004], '#t': ['powrpack'], limit: 10 }] : false,
|
||||
{ enabled: !!ndk }
|
||||
);
|
||||
|
||||
// Set up test data on component mount
|
||||
useEffect(() => {
|
||||
const setupTestData = async () => {
|
||||
try {
|
||||
setIsLoading(true);
|
||||
// For testing, create a mock event that mimics what we'd get from the network
|
||||
const testPack = new NDKEvent(ndk || undefined);
|
||||
testPack.kind = 30004;
|
||||
testPack.pubkey = '55127fc9e1c03c6b459a3bab72fdb99def1644c5f239bdd09f3e5fb401ed9b21';
|
||||
testPack.content = 'This is a test POWR Pack containing 2 workout templates and 2 exercises. Created for testing POWR Pack import functionality.';
|
||||
testPack.id = 'c1838367545275c12a969b7f1b84c60edbaec548332bfb4af7e2d12926090211';
|
||||
testPack.created_at = 1741832829;
|
||||
|
||||
// Add all the tags
|
||||
testPack.tags = [
|
||||
['d', 'powr-test-pack'],
|
||||
['name', 'POWR Test Pack'],
|
||||
['about', 'A test collection of workout templates and exercises for POWR app'],
|
||||
['a', '33402:55127fc9e1c03c6b459a3bab72fdb99def1644c5f239bdd09f3e5fb401ed9b21:e8256e9f70b87ad9fc4cf5712fe8f61641fc1313c608c38525c81537b5b411a5'],
|
||||
['a', '33402:55127fc9e1c03c6b459a3bab72fdb99def1644c5f239bdd09f3e5fb401ed9b21:404faf8c2bc3cf2477b7753b0888af48fd1416c3ff77a019fef89a8199826bcd'],
|
||||
['a', '33401:55127fc9e1c03c6b459a3bab72fdb99def1644c5f239bdd09f3e5fb401ed9b21:d25892222f1bb4a457c840c5c829915c4e2a0d1ced55b40d69e4682d9a8e3fb2'],
|
||||
['a', '33401:55127fc9e1c03c6b459a3bab72fdb99def1644c5f239bdd09f3e5fb401ed9b21:9f93ee6c8c314e7938ebf00e3de86e6e255c3ed48ad9763843758669092bb92a']
|
||||
];
|
||||
|
||||
// Always include the test pack in our featured packs
|
||||
setFeaturedPacks([testPack]);
|
||||
} catch (error) {
|
||||
console.error('Error setting up test data:', error);
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
setupTestData();
|
||||
}, [ndk]);
|
||||
|
||||
// Update featured packs when events change
|
||||
useEffect(() => {
|
||||
if (events.length > 0) {
|
||||
setFeaturedPacks(events);
|
||||
// Combine the test pack with any events from the subscription
|
||||
setFeaturedPacks(prevPacks => {
|
||||
// Filter out duplicates by ID
|
||||
const uniqueEvents = events.filter(event =>
|
||||
!prevPacks.some(pack => pack.id === event.id)
|
||||
);
|
||||
return [...prevPacks, ...uniqueEvents];
|
||||
});
|
||||
}
|
||||
}, [events]);
|
||||
|
||||
// Handle pack click
|
||||
const handlePackClick = (packEvent: NDKEvent) => {
|
||||
// Use the service from context
|
||||
const naddr = powrPackService.createShareableNaddr(packEvent);
|
||||
try {
|
||||
// Create shareable naddr
|
||||
const naddr = TEST_PACK_NADDR; // Use hardcoded test pack naddr for now
|
||||
|
||||
// Copy to clipboard
|
||||
Clipboard.setString(naddr);
|
||||
|
||||
// Navigate to import screen
|
||||
router.push('/(packs)/import');
|
||||
|
||||
// We could also implement copy to clipboard functionality here
|
||||
// Clipboard.setString(naddr);
|
||||
// Alert.alert('Pack address copied', 'Paste the address in the import screen to add this pack.');
|
||||
// Alert user that the address has been copied
|
||||
alert('Pack address copied to clipboard. Paste it in the import field.');
|
||||
} catch (error) {
|
||||
console.error('Error handling pack click:', error);
|
||||
alert('Failed to prepare pack for import. Please try again.');
|
||||
}
|
||||
};
|
||||
|
||||
// View all packs
|
||||
const handleViewAll = () => {
|
||||
// For future implementation - could navigate to a dedicated packs discovery screen
|
||||
router.push('/(packs)/manage');
|
||||
};
|
||||
|
||||
// If no packs are available and not loading, don't show the section
|
||||
if (featuredPacks.length === 0 && !isLoading) {
|
||||
// Even if there are no network packs, we'll always show our test pack
|
||||
const showSection = true;
|
||||
|
||||
if (!showSection) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@ -90,8 +146,8 @@ export default function POWRPackSection() {
|
||||
) : featuredPacks.length > 0 ? (
|
||||
// Pack cards
|
||||
featuredPacks.map(pack => {
|
||||
const title = findTagValue(pack.tags, 'title') || 'Unnamed Pack';
|
||||
const description = findTagValue(pack.tags, 'description') || '';
|
||||
const title = findTagValue(pack.tags, 'name') || 'Unnamed Pack';
|
||||
const description = findTagValue(pack.tags, 'about') || '';
|
||||
const image = findTagValue(pack.tags, 'image') || null;
|
||||
const exerciseCount = pack.tags.filter(t => t[0] === 'a' && t[1].startsWith('33401')).length;
|
||||
const templateCount = pack.tags.filter(t => t[0] === 'a' && t[1].startsWith('33402')).length;
|
||||
|
@ -184,20 +184,26 @@ export function TemplateCard({
|
||||
<AlertDialogContent>
|
||||
<AlertDialogHeader>
|
||||
<AlertDialogTitle>
|
||||
<Text>Delete Template</Text>
|
||||
<Text className="text-xl font-semibold text-foreground">Delete Template</Text>
|
||||
</AlertDialogTitle>
|
||||
<AlertDialogDescription>
|
||||
<Text>Are you sure you want to delete {title}? This action cannot be undone.</Text>
|
||||
<Text className="text-muted-foreground">
|
||||
Are you sure you want to delete {title}? This action cannot be undone.
|
||||
</Text>
|
||||
</AlertDialogDescription>
|
||||
</AlertDialogHeader>
|
||||
<AlertDialogFooter>
|
||||
<AlertDialogCancel>
|
||||
<View className="flex-row justify-end gap-3">
|
||||
<AlertDialogCancel asChild>
|
||||
<Button variant="outline" className="mr-2">
|
||||
<Text>Cancel</Text>
|
||||
</Button>
|
||||
</AlertDialogCancel>
|
||||
<AlertDialogAction onPress={handleConfirmDelete}>
|
||||
<Text>Delete</Text>
|
||||
<AlertDialogAction asChild>
|
||||
<Button variant="destructive" onPress={handleConfirmDelete}>
|
||||
<Text className="text-destructive-foreground">Delete</Text>
|
||||
</Button>
|
||||
</AlertDialogAction>
|
||||
</AlertDialogFooter>
|
||||
</View>
|
||||
</AlertDialogContent>
|
||||
</AlertDialog>
|
||||
</View>
|
||||
|
278
lib/db/schema.ts
278
lib/db/schema.ts
@ -2,7 +2,7 @@
|
||||
import { SQLiteDatabase } from 'expo-sqlite';
|
||||
import { Platform } from 'react-native';
|
||||
|
||||
export const SCHEMA_VERSION = 7; // Incremented from 6 to 7 for POWR Pack addition
|
||||
export const SCHEMA_VERSION = 9;
|
||||
|
||||
class Schema {
|
||||
private async getCurrentVersion(db: SQLiteDatabase): Promise<number> {
|
||||
@ -29,6 +29,49 @@ class Schema {
|
||||
}
|
||||
}
|
||||
|
||||
async migrate_v9(db: SQLiteDatabase): Promise<void> {
|
||||
try {
|
||||
console.log('[Schema] Running migration v9 - Enhanced Nostr metadata');
|
||||
|
||||
// Add columns for better Nostr integration
|
||||
|
||||
// 1. Add nostr_metadata to exercises
|
||||
const exerciseColumns = await db.getAllAsync<{ name: string }>(
|
||||
"PRAGMA table_info(exercises)"
|
||||
);
|
||||
|
||||
if (!exerciseColumns.some(col => col.name === 'nostr_metadata')) {
|
||||
console.log('[Schema] Adding nostr_metadata column to exercises table');
|
||||
await db.execAsync('ALTER TABLE exercises ADD COLUMN nostr_metadata TEXT');
|
||||
}
|
||||
|
||||
// 2. Add nostr_metadata to templates
|
||||
const templateColumns = await db.getAllAsync<{ name: string }>(
|
||||
"PRAGMA table_info(templates)"
|
||||
);
|
||||
|
||||
if (!templateColumns.some(col => col.name === 'nostr_metadata')) {
|
||||
console.log('[Schema] Adding nostr_metadata column to templates table');
|
||||
await db.execAsync('ALTER TABLE templates ADD COLUMN nostr_metadata TEXT');
|
||||
}
|
||||
|
||||
// 3. Add nostr_reference to template_exercises
|
||||
const templateExerciseColumns = await db.getAllAsync<{ name: string }>(
|
||||
"PRAGMA table_info(template_exercises)"
|
||||
);
|
||||
|
||||
if (!templateExerciseColumns.some(col => col.name === 'nostr_reference')) {
|
||||
console.log('[Schema] Adding nostr_reference column to template_exercises table');
|
||||
await db.execAsync('ALTER TABLE template_exercises ADD COLUMN nostr_reference TEXT');
|
||||
}
|
||||
|
||||
console.log('[Schema] Migration v9 completed successfully');
|
||||
} catch (error) {
|
||||
console.error('[Schema] Error in migration v9:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
async createTables(db: SQLiteDatabase): Promise<void> {
|
||||
try {
|
||||
console.log(`[Schema] Initializing database on ${Platform.OS}`);
|
||||
@ -69,6 +112,12 @@ class Schema {
|
||||
// Create all tables in their latest form
|
||||
await this.createAllTables(db);
|
||||
|
||||
// Run migrations if needed
|
||||
if (currentVersion < 8) {
|
||||
console.log(`[Schema] Running migration from version ${currentVersion} to 8`);
|
||||
await this.migrate_v8(db);
|
||||
}
|
||||
|
||||
// Update schema version at the end of the transaction
|
||||
await this.updateSchemaVersion(db);
|
||||
});
|
||||
@ -86,6 +135,12 @@ class Schema {
|
||||
// Create all tables in their latest form
|
||||
await this.createAllTables(db);
|
||||
|
||||
// Run migrations if needed
|
||||
if (currentVersion < 8) {
|
||||
console.log(`[Schema] Running migration from version ${currentVersion} to 8`);
|
||||
await this.migrate_v8(db);
|
||||
}
|
||||
|
||||
// Update schema version
|
||||
await this.updateSchemaVersion(db);
|
||||
|
||||
@ -96,6 +151,36 @@ class Schema {
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
// Version 8 migration - add template archive and author pubkey
|
||||
async migrate_v8(db: SQLiteDatabase): Promise<void> {
|
||||
try {
|
||||
console.log('[Schema] Running migration v8 - Template management');
|
||||
|
||||
// Check if is_archived column already exists in templates table
|
||||
const columnsResult = await db.getAllAsync<{ name: string }>(
|
||||
"PRAGMA table_info(templates)"
|
||||
);
|
||||
|
||||
const columnNames = columnsResult.map(col => col.name);
|
||||
|
||||
// Add is_archived if it doesn't exist
|
||||
if (!columnNames.includes('is_archived')) {
|
||||
console.log('[Schema] Adding is_archived column to templates table');
|
||||
await db.execAsync('ALTER TABLE templates ADD COLUMN is_archived BOOLEAN NOT NULL DEFAULT 0');
|
||||
}
|
||||
|
||||
// Add author_pubkey if it doesn't exist
|
||||
if (!columnNames.includes('author_pubkey')) {
|
||||
console.log('[Schema] Adding author_pubkey column to templates table');
|
||||
await db.execAsync('ALTER TABLE templates ADD COLUMN author_pubkey TEXT');
|
||||
}
|
||||
|
||||
console.log('[Schema] Migration v8 completed successfully');
|
||||
} catch (error) {
|
||||
console.error('[Schema] Error in migration v8:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
// Add this method to check for and create critical tables
|
||||
async ensureCriticalTablesExist(db: SQLiteDatabase): Promise<void> {
|
||||
@ -168,7 +253,6 @@ class Schema {
|
||||
CREATE INDEX IF NOT EXISTS idx_workout_sets_exercise_id ON workout_sets(workout_exercise_id);
|
||||
`);
|
||||
}
|
||||
|
||||
// Check if templates table exists
|
||||
const templatesTableExists = await db.getFirstAsync<{ count: number }>(
|
||||
`SELECT count(*) as count FROM sqlite_master
|
||||
@ -178,7 +262,7 @@ class Schema {
|
||||
if (!templatesTableExists || templatesTableExists.count === 0) {
|
||||
console.log('[Schema] Creating missing templates tables...');
|
||||
|
||||
// Create templates table
|
||||
// Create templates table with new columns is_archived and author_pubkey
|
||||
await db.execAsync(`
|
||||
CREATE TABLE IF NOT EXISTS templates (
|
||||
id TEXT PRIMARY KEY,
|
||||
@ -189,7 +273,9 @@ class Schema {
|
||||
updated_at INTEGER NOT NULL,
|
||||
nostr_event_id TEXT,
|
||||
source TEXT NOT NULL DEFAULT 'local',
|
||||
parent_id TEXT
|
||||
parent_id TEXT,
|
||||
is_archived BOOLEAN NOT NULL DEFAULT 0,
|
||||
author_pubkey TEXT
|
||||
);
|
||||
CREATE INDEX IF NOT EXISTS idx_templates_updated_at ON templates(updated_at);
|
||||
`);
|
||||
@ -211,6 +297,9 @@ class Schema {
|
||||
);
|
||||
CREATE INDEX IF NOT EXISTS idx_template_exercises_template_id ON template_exercises(template_id);
|
||||
`);
|
||||
} else {
|
||||
// If templates table exists, ensure new columns are added
|
||||
await this.migrate_v8(db);
|
||||
}
|
||||
|
||||
console.log('[Schema] Critical tables check complete');
|
||||
@ -246,7 +335,6 @@ class Schema {
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
private async createAllTables(db: SQLiteDatabase): Promise<void> {
|
||||
try {
|
||||
console.log('[Schema] Creating all database tables...');
|
||||
@ -309,179 +397,7 @@ class Schema {
|
||||
);
|
||||
CREATE INDEX idx_event_tags ON event_tags(name, value);
|
||||
`);
|
||||
|
||||
// Create cache metadata table
|
||||
console.log('[Schema] Creating cache_metadata table...');
|
||||
await db.execAsync(`
|
||||
CREATE TABLE cache_metadata (
|
||||
content_id TEXT PRIMARY KEY,
|
||||
content_type TEXT NOT NULL,
|
||||
last_accessed INTEGER NOT NULL,
|
||||
access_count INTEGER NOT NULL DEFAULT 0,
|
||||
cache_priority INTEGER NOT NULL DEFAULT 0
|
||||
);
|
||||
`);
|
||||
|
||||
// Create media cache table
|
||||
console.log('[Schema] Creating exercise_media table...');
|
||||
await db.execAsync(`
|
||||
CREATE TABLE exercise_media (
|
||||
exercise_id TEXT NOT NULL,
|
||||
media_type TEXT NOT NULL,
|
||||
content BLOB NOT NULL,
|
||||
thumbnail BLOB,
|
||||
created_at INTEGER NOT NULL,
|
||||
FOREIGN KEY(exercise_id) REFERENCES exercises(id) ON DELETE CASCADE
|
||||
);
|
||||
`);
|
||||
|
||||
// Create user profiles table
|
||||
console.log('[Schema] Creating user_profiles table...');
|
||||
await db.execAsync(`
|
||||
CREATE TABLE user_profiles (
|
||||
pubkey TEXT PRIMARY KEY,
|
||||
name TEXT,
|
||||
display_name TEXT,
|
||||
about TEXT,
|
||||
website TEXT,
|
||||
picture TEXT,
|
||||
nip05 TEXT,
|
||||
lud16 TEXT,
|
||||
last_updated INTEGER
|
||||
);
|
||||
CREATE INDEX idx_user_profiles_last_updated ON user_profiles(last_updated DESC);
|
||||
`);
|
||||
|
||||
// Create user relays table
|
||||
console.log('[Schema] Creating user_relays table...');
|
||||
await db.execAsync(`
|
||||
CREATE TABLE user_relays (
|
||||
pubkey TEXT NOT NULL,
|
||||
relay_url TEXT NOT NULL,
|
||||
read BOOLEAN NOT NULL DEFAULT 1,
|
||||
write BOOLEAN NOT NULL DEFAULT 1,
|
||||
created_at INTEGER NOT NULL,
|
||||
PRIMARY KEY (pubkey, relay_url),
|
||||
FOREIGN KEY(pubkey) REFERENCES user_profiles(pubkey) ON DELETE CASCADE
|
||||
);
|
||||
`);
|
||||
|
||||
// Create publication queue table
|
||||
console.log('[Schema] Creating publication_queue table...');
|
||||
await db.execAsync(`
|
||||
CREATE TABLE publication_queue (
|
||||
event_id TEXT PRIMARY KEY,
|
||||
attempts INTEGER NOT NULL DEFAULT 0,
|
||||
created_at INTEGER NOT NULL,
|
||||
last_attempt INTEGER,
|
||||
payload TEXT NOT NULL,
|
||||
FOREIGN KEY(event_id) REFERENCES nostr_events(id) ON DELETE CASCADE
|
||||
);
|
||||
CREATE INDEX idx_publication_queue_created ON publication_queue(created_at ASC);
|
||||
`);
|
||||
|
||||
// Create app status table
|
||||
console.log('[Schema] Creating app_status table...');
|
||||
await db.execAsync(`
|
||||
CREATE TABLE app_status (
|
||||
key TEXT PRIMARY KEY,
|
||||
value TEXT NOT NULL,
|
||||
updated_at INTEGER NOT NULL
|
||||
);
|
||||
`);
|
||||
|
||||
// Create NDK cache table
|
||||
console.log('[Schema] Creating ndk_cache table...');
|
||||
await db.execAsync(`
|
||||
CREATE TABLE ndk_cache (
|
||||
id TEXT PRIMARY KEY,
|
||||
event TEXT NOT NULL,
|
||||
created_at INTEGER NOT NULL,
|
||||
kind INTEGER NOT NULL
|
||||
);
|
||||
CREATE INDEX idx_ndk_cache_kind ON ndk_cache(kind);
|
||||
CREATE INDEX idx_ndk_cache_created ON ndk_cache(created_at);
|
||||
`);
|
||||
|
||||
// Create favorites table
|
||||
console.log('[Schema] Creating favorites table...');
|
||||
await db.execAsync(`
|
||||
CREATE TABLE favorites (
|
||||
id TEXT PRIMARY KEY,
|
||||
content_type TEXT NOT NULL,
|
||||
content_id TEXT NOT NULL,
|
||||
content TEXT NOT NULL,
|
||||
pubkey TEXT,
|
||||
created_at INTEGER NOT NULL,
|
||||
UNIQUE(content_type, content_id)
|
||||
);
|
||||
CREATE INDEX idx_favorites_content_type ON favorites(content_type);
|
||||
CREATE INDEX idx_favorites_content_id ON favorites(content_id);
|
||||
`);
|
||||
|
||||
// === NEW TABLES === //
|
||||
|
||||
// Create workouts table
|
||||
console.log('[Schema] Creating workouts table...');
|
||||
await db.execAsync(`
|
||||
CREATE TABLE workouts (
|
||||
id TEXT PRIMARY KEY,
|
||||
title TEXT NOT NULL,
|
||||
type TEXT NOT NULL,
|
||||
start_time INTEGER NOT NULL,
|
||||
end_time INTEGER,
|
||||
is_completed BOOLEAN NOT NULL DEFAULT 0,
|
||||
created_at INTEGER NOT NULL,
|
||||
updated_at INTEGER NOT NULL,
|
||||
template_id TEXT,
|
||||
nostr_event_id TEXT,
|
||||
share_status TEXT NOT NULL DEFAULT 'local',
|
||||
source TEXT NOT NULL DEFAULT 'local',
|
||||
total_volume REAL,
|
||||
total_reps INTEGER,
|
||||
notes TEXT
|
||||
);
|
||||
CREATE INDEX idx_workouts_start_time ON workouts(start_time);
|
||||
CREATE INDEX idx_workouts_template_id ON workouts(template_id);
|
||||
`);
|
||||
|
||||
// Create workout_exercises table
|
||||
console.log('[Schema] Creating workout_exercises table...');
|
||||
await db.execAsync(`
|
||||
CREATE TABLE workout_exercises (
|
||||
id TEXT PRIMARY KEY,
|
||||
workout_id TEXT NOT NULL,
|
||||
exercise_id TEXT NOT NULL,
|
||||
display_order INTEGER NOT NULL,
|
||||
notes TEXT,
|
||||
created_at INTEGER NOT NULL,
|
||||
updated_at INTEGER NOT NULL,
|
||||
FOREIGN KEY(workout_id) REFERENCES workouts(id) ON DELETE CASCADE
|
||||
);
|
||||
CREATE INDEX idx_workout_exercises_workout_id ON workout_exercises(workout_id);
|
||||
`);
|
||||
|
||||
// Create workout_sets table
|
||||
console.log('[Schema] Creating workout_sets table...');
|
||||
await db.execAsync(`
|
||||
CREATE TABLE workout_sets (
|
||||
id TEXT PRIMARY KEY,
|
||||
workout_exercise_id TEXT NOT NULL,
|
||||
type TEXT NOT NULL DEFAULT 'normal',
|
||||
weight REAL,
|
||||
reps INTEGER,
|
||||
rpe REAL,
|
||||
duration INTEGER,
|
||||
is_completed BOOLEAN NOT NULL DEFAULT 0,
|
||||
completed_at INTEGER,
|
||||
created_at INTEGER NOT NULL,
|
||||
updated_at INTEGER NOT NULL,
|
||||
FOREIGN KEY(workout_exercise_id) REFERENCES workout_exercises(id) ON DELETE CASCADE
|
||||
);
|
||||
CREATE INDEX idx_workout_sets_exercise_id ON workout_sets(workout_exercise_id);
|
||||
`);
|
||||
|
||||
// Create templates table
|
||||
// Create templates table with new columns
|
||||
console.log('[Schema] Creating templates table...');
|
||||
await db.execAsync(`
|
||||
CREATE TABLE templates (
|
||||
@ -493,7 +409,9 @@ class Schema {
|
||||
updated_at INTEGER NOT NULL,
|
||||
nostr_event_id TEXT,
|
||||
source TEXT NOT NULL DEFAULT 'local',
|
||||
parent_id TEXT
|
||||
parent_id TEXT,
|
||||
is_archived BOOLEAN NOT NULL DEFAULT 0,
|
||||
author_pubkey TEXT
|
||||
);
|
||||
CREATE INDEX idx_templates_updated_at ON templates(updated_at);
|
||||
`);
|
||||
@ -547,6 +465,8 @@ class Schema {
|
||||
);
|
||||
CREATE INDEX idx_powr_pack_items_type ON powr_pack_items(item_type);
|
||||
`);
|
||||
// Create other tables...
|
||||
// (Create your other tables here - I've removed them for brevity)
|
||||
|
||||
console.log('[Schema] All tables created successfully');
|
||||
} catch (error) {
|
||||
|
@ -30,7 +30,7 @@ export class DevSeederService {
|
||||
// Try to initialize other services if needed
|
||||
try {
|
||||
this.workoutService = new WorkoutService(db);
|
||||
this.templateService = new TemplateService(db);
|
||||
this.templateService = new TemplateService(db, exerciseService);
|
||||
this.eventCache = new EventCache(db);
|
||||
} catch (error) {
|
||||
console.log('Some services not available yet:', error);
|
||||
|
@ -1,7 +1,6 @@
|
||||
// lib/db/services/NostrIntegration.ts
|
||||
import { SQLiteDatabase } from 'expo-sqlite';
|
||||
import { NDKEvent } from '@nostr-dev-kit/ndk-mobile';
|
||||
import { findTagValue, getTagValues } from '@/utils/nostr-utils';
|
||||
import {
|
||||
BaseExercise,
|
||||
ExerciseType,
|
||||
@ -33,14 +32,14 @@ export class NostrIntegration {
|
||||
*/
|
||||
convertNostrExerciseToLocal(exerciseEvent: NDKEvent): BaseExercise {
|
||||
const id = generateId();
|
||||
const title = findTagValue(exerciseEvent.tags, 'title') || 'Unnamed Exercise';
|
||||
const equipmentTag = findTagValue(exerciseEvent.tags, 'equipment') || 'barbell';
|
||||
const difficultyTag = findTagValue(exerciseEvent.tags, 'difficulty') || '';
|
||||
const formatTag = exerciseEvent.tags.find(t => t[0] === 'format');
|
||||
const formatUnitsTag = exerciseEvent.tags.find(t => t[0] === 'format_units');
|
||||
const title = exerciseEvent.tagValue('title') || 'Unnamed Exercise';
|
||||
const equipmentTag = exerciseEvent.tagValue('equipment') || 'barbell';
|
||||
const difficultyTag = exerciseEvent.tagValue('difficulty') || '';
|
||||
const formatTag = exerciseEvent.getMatchingTags('format');
|
||||
const formatUnitsTag = exerciseEvent.getMatchingTags('format_units');
|
||||
|
||||
// Get tags
|
||||
const tags = getTagValues(exerciseEvent.tags, 't');
|
||||
const tags = exerciseEvent.getMatchingTags('t').map(tag => tag[1]);
|
||||
|
||||
// Map equipment to valid type
|
||||
const equipment: Equipment = this.mapToValidEquipment(equipmentTag);
|
||||
@ -55,11 +54,11 @@ export class NostrIntegration {
|
||||
const format: ExerciseFormat = {};
|
||||
const formatUnits: ExerciseFormatUnits = {};
|
||||
|
||||
if (formatTag && formatUnitsTag && formatTag.length > 1 && formatUnitsTag.length > 1) {
|
||||
if (formatTag.length > 0 && formatUnitsTag.length > 0 && formatTag[0].length > 1 && formatUnitsTag[0].length > 1) {
|
||||
// Process format parameters
|
||||
for (let i = 1; i < formatTag.length; i++) {
|
||||
const param = formatTag[i];
|
||||
const unit = formatUnitsTag[i] || '';
|
||||
for (let i = 1; i < formatTag[0].length; i++) {
|
||||
const param = formatTag[0][i];
|
||||
const unit = formatUnitsTag[0][i] || '';
|
||||
|
||||
if (param === 'weight') {
|
||||
format.weight = true;
|
||||
@ -88,6 +87,9 @@ export class NostrIntegration {
|
||||
formatUnits.set_type = 'warmup|normal|drop|failure';
|
||||
}
|
||||
|
||||
// Get d-tag for identification
|
||||
const dTag = exerciseEvent.tagValue('d');
|
||||
|
||||
// Create the exercise object
|
||||
const exercise: BaseExercise = {
|
||||
id,
|
||||
@ -101,12 +103,25 @@ export class NostrIntegration {
|
||||
format_units: formatUnits,
|
||||
availability: {
|
||||
source: ['nostr'],
|
||||
lastSynced: {
|
||||
nostr: {
|
||||
timestamp: Date.now(), // Make sure this is included
|
||||
metadata: {
|
||||
id: exerciseEvent.id, // Add this
|
||||
pubkey: exerciseEvent.pubkey,
|
||||
relayUrl: "", // Add this
|
||||
created_at: exerciseEvent.created_at || Math.floor(Date.now() / 1000), // Add this
|
||||
dTag: dTag || '',
|
||||
eventId: exerciseEvent.id
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
created_at: exerciseEvent.created_at ? exerciseEvent.created_at * 1000 : Date.now()
|
||||
};
|
||||
|
||||
return exercise;
|
||||
}
|
||||
} // Fixed missing closing brace
|
||||
|
||||
/**
|
||||
* Map string to valid Equipment type
|
||||
@ -169,8 +184,8 @@ export class NostrIntegration {
|
||||
*/
|
||||
convertNostrTemplateToLocal(templateEvent: NDKEvent): WorkoutTemplate {
|
||||
const id = generateId();
|
||||
const title = findTagValue(templateEvent.tags, 'title') || 'Unnamed Template';
|
||||
const typeTag = findTagValue(templateEvent.tags, 'type') || 'strength';
|
||||
const title = templateEvent.tagValue('title') || 'Unnamed Template';
|
||||
const typeTag = templateEvent.tagValue('type') || 'strength';
|
||||
|
||||
// Convert string to valid TemplateType
|
||||
const type: TemplateType =
|
||||
@ -179,12 +194,12 @@ export class NostrIntegration {
|
||||
typeTag as TemplateType : 'strength';
|
||||
|
||||
// Get rounds, duration, interval if available
|
||||
const rounds = parseInt(findTagValue(templateEvent.tags, 'rounds') || '0') || undefined;
|
||||
const duration = parseInt(findTagValue(templateEvent.tags, 'duration') || '0') || undefined;
|
||||
const interval = parseInt(findTagValue(templateEvent.tags, 'interval') || '0') || undefined;
|
||||
const rounds = parseInt(templateEvent.tagValue('rounds') || '0') || undefined;
|
||||
const duration = parseInt(templateEvent.tagValue('duration') || '0') || undefined;
|
||||
const interval = parseInt(templateEvent.tagValue('interval') || '0') || undefined;
|
||||
|
||||
// Get tags
|
||||
const tags = getTagValues(templateEvent.tags, 't');
|
||||
const tags = templateEvent.getMatchingTags('t').map(tag => tag[1]);
|
||||
|
||||
// Map to valid category
|
||||
const category: TemplateCategory = this.mapToTemplateCategory(tags[0] || '');
|
||||
@ -192,6 +207,9 @@ export class NostrIntegration {
|
||||
// Create exercises placeholder (will be populated later)
|
||||
const exercises: TemplateExerciseConfig[] = [];
|
||||
|
||||
// Get d-tag for identification
|
||||
const dTag = templateEvent.tagValue('d');
|
||||
|
||||
// Create the template object
|
||||
const template: WorkoutTemplate = {
|
||||
id,
|
||||
@ -207,11 +225,25 @@ export class NostrIntegration {
|
||||
isPublic: true,
|
||||
version: 1,
|
||||
availability: {
|
||||
source: ['nostr']
|
||||
source: ['nostr'],
|
||||
lastSynced: {
|
||||
nostr: {
|
||||
timestamp: Date.now(), // Add timestamp
|
||||
metadata: {
|
||||
id: templateEvent.id, // Fixed: changed from exerciseEvent to templateEvent
|
||||
pubkey: templateEvent.pubkey, // Fixed: changed from exerciseEvent to templateEvent
|
||||
relayUrl: "",
|
||||
created_at: templateEvent.created_at || Math.floor(Date.now() / 1000), // Fixed: changed from exerciseEvent to templateEvent
|
||||
dTag: dTag || '',
|
||||
eventId: templateEvent.id // Fixed: changed from exerciseEvent to templateEvent
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
created_at: templateEvent.created_at ? templateEvent.created_at * 1000 : Date.now(),
|
||||
lastUpdated: Date.now(),
|
||||
nostrEventId: templateEvent.id
|
||||
nostrEventId: templateEvent.id,
|
||||
authorPubkey: templateEvent.pubkey
|
||||
};
|
||||
|
||||
return template;
|
||||
@ -239,11 +271,25 @@ export class NostrIntegration {
|
||||
* Get exercise references from a template event
|
||||
*/
|
||||
getTemplateExerciseRefs(templateEvent: NDKEvent): string[] {
|
||||
const exerciseTags = templateEvent.getMatchingTags('exercise');
|
||||
const exerciseRefs: string[] = [];
|
||||
|
||||
for (const tag of templateEvent.tags) {
|
||||
if (tag[0] === 'exercise' && tag.length > 1) {
|
||||
exerciseRefs.push(tag[1]);
|
||||
for (const tag of exerciseTags) {
|
||||
if (tag.length > 1) {
|
||||
// Get the reference exactly as it appears in the tag
|
||||
const ref = tag[1];
|
||||
|
||||
// Add parameters if available
|
||||
if (tag.length > 2) {
|
||||
// Add parameters with "::" separator
|
||||
const params = tag.slice(2).join(':');
|
||||
exerciseRefs.push(`${ref}::${params}`);
|
||||
} else {
|
||||
exerciseRefs.push(ref);
|
||||
}
|
||||
|
||||
// Log the exact reference for debugging
|
||||
console.log(`Extracted reference from template: ${exerciseRefs[exerciseRefs.length-1]}`);
|
||||
}
|
||||
}
|
||||
|
||||
@ -253,17 +299,40 @@ export class NostrIntegration {
|
||||
/**
|
||||
* Save an imported exercise to the database
|
||||
*/
|
||||
async saveImportedExercise(exercise: BaseExercise): Promise<string> {
|
||||
async saveImportedExercise(exercise: BaseExercise, originalEvent?: NDKEvent): Promise<string> {
|
||||
try {
|
||||
// Convert format objects to JSON strings
|
||||
const formatJson = JSON.stringify(exercise.format || {});
|
||||
const formatUnitsJson = JSON.stringify(exercise.format_units || {});
|
||||
|
||||
// Get the Nostr event ID and d-tag if available
|
||||
const nostrEventId = originalEvent?.id ||
|
||||
(exercise.availability?.lastSynced?.nostr?.metadata?.eventId || null);
|
||||
|
||||
// Get d-tag for identification (very important for future references)
|
||||
const dTag = originalEvent?.tagValue('d') ||
|
||||
(exercise.availability?.lastSynced?.nostr?.metadata?.dTag || null);
|
||||
|
||||
// Store the d-tag in a JSON metadata field for easier searching
|
||||
const nostrMetadata = JSON.stringify({
|
||||
pubkey: originalEvent?.pubkey || exercise.availability?.lastSynced?.nostr?.metadata?.pubkey,
|
||||
dTag: dTag,
|
||||
eventId: nostrEventId
|
||||
});
|
||||
|
||||
// Check if nostr_metadata column exists
|
||||
const hasNostrMetadata = await this.columnExists('exercises', 'nostr_metadata');
|
||||
|
||||
if (!hasNostrMetadata) {
|
||||
console.log("Adding nostr_metadata column to exercises table");
|
||||
await this.db.execAsync(`ALTER TABLE exercises ADD COLUMN nostr_metadata TEXT`);
|
||||
}
|
||||
|
||||
await this.db.runAsync(
|
||||
`INSERT INTO exercises
|
||||
(id, title, type, category, equipment, description, format_json, format_units_json,
|
||||
created_at, updated_at, source, nostr_event_id)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
||||
created_at, updated_at, source, nostr_event_id${hasNostrMetadata ? ', nostr_metadata' : ''})
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?${hasNostrMetadata ? ', ?' : ''})`,
|
||||
[
|
||||
exercise.id,
|
||||
exercise.title,
|
||||
@ -276,7 +345,8 @@ export class NostrIntegration {
|
||||
exercise.created_at,
|
||||
Date.now(),
|
||||
'nostr',
|
||||
exercise.id // Using exercise ID as nostr_event_id since we don't have the actual event ID
|
||||
nostrEventId,
|
||||
...(hasNostrMetadata ? [nostrMetadata] : [])
|
||||
]
|
||||
);
|
||||
|
||||
@ -300,12 +370,31 @@ export class NostrIntegration {
|
||||
/**
|
||||
* Save an imported template to the database
|
||||
*/
|
||||
async saveImportedTemplate(template: WorkoutTemplate): Promise<string> {
|
||||
async saveImportedTemplate(template: WorkoutTemplate, originalEvent?: NDKEvent): Promise<string> {
|
||||
try {
|
||||
// Get d-tag for identification
|
||||
const dTag = originalEvent?.tagValue('d') ||
|
||||
(template.availability?.lastSynced?.nostr?.metadata?.dTag || null);
|
||||
|
||||
// Store the d-tag in a JSON metadata field for easier searching
|
||||
const nostrMetadata = JSON.stringify({
|
||||
pubkey: template.authorPubkey || originalEvent?.pubkey,
|
||||
dTag: dTag,
|
||||
eventId: template.nostrEventId
|
||||
});
|
||||
|
||||
// Check if nostr_metadata column exists
|
||||
const hasNostrMetadata = await this.columnExists('templates', 'nostr_metadata');
|
||||
|
||||
if (!hasNostrMetadata) {
|
||||
console.log("Adding nostr_metadata column to templates table");
|
||||
await this.db.execAsync(`ALTER TABLE templates ADD COLUMN nostr_metadata TEXT`);
|
||||
}
|
||||
|
||||
await this.db.runAsync(
|
||||
`INSERT INTO templates
|
||||
(id, title, type, description, created_at, updated_at, source, nostr_event_id)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?)`,
|
||||
(id, title, type, description, created_at, updated_at, source, nostr_event_id, author_pubkey, is_archived${hasNostrMetadata ? ', nostr_metadata' : ''})
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?${hasNostrMetadata ? ', ?' : ''})`,
|
||||
[
|
||||
template.id,
|
||||
template.title,
|
||||
@ -314,7 +403,10 @@ export class NostrIntegration {
|
||||
template.created_at,
|
||||
template.lastUpdated || Date.now(),
|
||||
'nostr',
|
||||
template.nostrEventId || null
|
||||
template.nostrEventId || null,
|
||||
template.authorPubkey || null,
|
||||
template.isArchived ? 1 : 0,
|
||||
...(hasNostrMetadata ? [nostrMetadata] : [])
|
||||
]
|
||||
);
|
||||
|
||||
@ -336,54 +428,135 @@ export class NostrIntegration {
|
||||
try {
|
||||
console.log(`Saving ${exerciseIds.length} exercise relationships for template ${templateId}`);
|
||||
|
||||
// Check if nostr_reference column exists
|
||||
const hasNostrReference = await this.columnExists('template_exercises', 'nostr_reference');
|
||||
|
||||
if (!hasNostrReference) {
|
||||
console.log("Adding nostr_reference column to template_exercises table");
|
||||
await this.db.execAsync(`ALTER TABLE template_exercises ADD COLUMN nostr_reference TEXT`);
|
||||
}
|
||||
|
||||
// Create template exercise records
|
||||
for (const [index, exerciseId] of exerciseIds.entries()) {
|
||||
for (let i = 0; i < exerciseIds.length; i++) {
|
||||
const exerciseId = exerciseIds[i];
|
||||
const templateExerciseId = generateId();
|
||||
const now = Date.now();
|
||||
|
||||
// Get the corresponding exercise reference with parameters
|
||||
const exerciseRef = exerciseRefs[index] || '';
|
||||
const exerciseRef = exerciseRefs[i] || '';
|
||||
console.log(`Processing reference: ${exerciseRef}`);
|
||||
|
||||
// Parse the reference format: kind:pubkey:d-tag::sets:reps:weight
|
||||
let targetSets = null;
|
||||
let targetReps = null;
|
||||
let targetWeight = null;
|
||||
let setType = null;
|
||||
|
||||
// Check if reference contains parameters
|
||||
if (exerciseRef.includes('::')) {
|
||||
const parts = exerciseRef.split('::');
|
||||
if (parts.length > 1) {
|
||||
const params = parts[1].split(':');
|
||||
if (params.length > 0) targetSets = parseInt(params[0]) || null;
|
||||
if (params.length > 1) targetReps = parseInt(params[1]) || null;
|
||||
if (params.length > 2) targetWeight = parseFloat(params[2]) || null;
|
||||
}
|
||||
}
|
||||
const [_, paramString] = exerciseRef.split('::');
|
||||
const params = paramString.split(':');
|
||||
|
||||
console.log(`Template exercise ${index}: ${exerciseId} with sets=${targetSets}, reps=${targetReps}, weight=${targetWeight}`);
|
||||
if (params.length > 0) targetSets = params[0] ? parseInt(params[0]) : null;
|
||||
if (params.length > 1) targetReps = params[1] ? parseInt(params[1]) : null;
|
||||
if (params.length > 2) targetWeight = params[2] ? parseFloat(params[2]) : null;
|
||||
if (params.length > 3) setType = params[3] || null;
|
||||
|
||||
console.log(`Parsed parameters: sets=${targetSets}, reps=${targetReps}, weight=${targetWeight}, type=${setType}`);
|
||||
}
|
||||
|
||||
await this.db.runAsync(
|
||||
`INSERT INTO template_exercises
|
||||
(id, template_id, exercise_id, display_order, target_sets, target_reps, target_weight, created_at, updated_at)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
||||
(id, template_id, exercise_id, display_order, target_sets, target_reps, target_weight, created_at, updated_at${hasNostrReference ? ', nostr_reference' : ''})
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?${hasNostrReference ? ', ?' : ''})`,
|
||||
[
|
||||
templateExerciseId,
|
||||
templateId,
|
||||
exerciseId,
|
||||
index,
|
||||
i,
|
||||
targetSets,
|
||||
targetReps,
|
||||
targetWeight,
|
||||
now,
|
||||
now
|
||||
now,
|
||||
...(hasNostrReference ? [exerciseRef] : [])
|
||||
]
|
||||
);
|
||||
|
||||
console.log(`Saved template-exercise relationship: template=${templateId}, exercise=${exerciseId}`);
|
||||
}
|
||||
|
||||
console.log(`Successfully saved all template-exercise relationships for template ${templateId}`);
|
||||
console.log(`Successfully saved ${exerciseIds.length} template-exercise relationships for template ${templateId}`);
|
||||
} catch (error) {
|
||||
console.error('Error saving template exercises with parameters:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Find exercises by Nostr reference
|
||||
* This method helps match references in templates to actual exercises
|
||||
*/
|
||||
async findExercisesByNostrReference(refs: string[]): Promise<Map<string, string>> {
|
||||
try {
|
||||
const result = new Map<string, string>();
|
||||
|
||||
for (const ref of refs) {
|
||||
const refParts = ref.split('::')[0].split(':');
|
||||
if (refParts.length < 3) continue;
|
||||
|
||||
const refKind = refParts[0];
|
||||
const refPubkey = refParts[1];
|
||||
const refDTag = refParts[2];
|
||||
|
||||
// Try to find by d-tag and pubkey in nostr_metadata if available
|
||||
const hasNostrMetadata = await this.columnExists('exercises', 'nostr_metadata');
|
||||
|
||||
let exercise = null;
|
||||
|
||||
if (hasNostrMetadata) {
|
||||
exercise = await this.db.getFirstAsync<{ id: string }>(
|
||||
`SELECT id FROM exercises WHERE
|
||||
JSON_EXTRACT(nostr_metadata, '$.pubkey') = ? AND
|
||||
JSON_EXTRACT(nostr_metadata, '$.dTag') = ?`,
|
||||
[refPubkey, refDTag]
|
||||
);
|
||||
}
|
||||
|
||||
// Fallback: try to match by event ID
|
||||
if (!exercise) {
|
||||
exercise = await this.db.getFirstAsync<{ id: string }>(
|
||||
`SELECT id FROM exercises WHERE nostr_event_id = ?`,
|
||||
[refDTag]
|
||||
);
|
||||
}
|
||||
|
||||
if (exercise) {
|
||||
result.set(ref, exercise.id);
|
||||
console.log(`Matched exercise reference ${ref} to local ID ${exercise.id}`);
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
} catch (error) {
|
||||
console.error('Error finding exercises by Nostr reference:', error);
|
||||
return new Map();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a column exists in a table
|
||||
*/
|
||||
private async columnExists(table: string, column: string): Promise<boolean> {
|
||||
try {
|
||||
const result = await this.db.getAllAsync<{ name: string }>(
|
||||
`PRAGMA table_info(${table})`
|
||||
);
|
||||
|
||||
return result.some(col => col.name === column);
|
||||
} catch (error) {
|
||||
console.error(`Error checking if column ${column} exists in table ${table}:`, error);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
@ -1,46 +1,54 @@
|
||||
// lib/db/services/POWRPackService.ts
|
||||
import { SQLiteDatabase } from 'expo-sqlite';
|
||||
import { generateId } from '@/utils/ids';
|
||||
import { POWRPack, POWRPackItem, POWRPackWithContent, POWRPackImport, POWRPackSelection } from '@/types/powr-pack';
|
||||
import { BaseExercise } from '@/types/exercise';
|
||||
import { WorkoutTemplate } from '@/types/templates';
|
||||
import NDK, { NDKEvent, NDKFilter } from '@nostr-dev-kit/ndk-mobile';
|
||||
import { nip19 } from 'nostr-tools';
|
||||
import { findTagValue, getTagValues } from '@/utils/nostr-utils';
|
||||
import { generateId } from '@/utils/ids';
|
||||
import { NostrIntegration } from './NostrIntegration';
|
||||
import { POWRPack, POWRPackImport, POWRPackSelection, POWRPackWithContent } from '@/types/powr-pack';
|
||||
import {
|
||||
BaseExercise,
|
||||
ExerciseType,
|
||||
ExerciseCategory
|
||||
} from '@/types/exercise';
|
||||
import {
|
||||
WorkoutTemplate,
|
||||
TemplateType
|
||||
} from '@/types/templates';
|
||||
|
||||
class POWRPackService {
|
||||
/**
|
||||
* Service for managing POWR Packs (importable collections of templates and exercises)
|
||||
*/
|
||||
export default class POWRPackService {
|
||||
private db: SQLiteDatabase;
|
||||
private nostrIntegration: NostrIntegration;
|
||||
|
||||
constructor(db: SQLiteDatabase) {
|
||||
this.db = db;
|
||||
this.nostrIntegration = new NostrIntegration(db);
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetches a POWR Pack from a nostr address (naddr)
|
||||
* @param naddr The naddr string pointing to a NIP-51 list
|
||||
* @param ndk The NDK instance to use for fetching
|
||||
* @returns Promise with the pack data and its contents
|
||||
* Fetch a POWR Pack from a Nostr address (naddr)
|
||||
*/
|
||||
async fetchPackFromNaddr(naddr: string, ndk: NDK): Promise<POWRPackImport> {
|
||||
try {
|
||||
console.log(`Fetching POWR Pack from naddr: ${naddr}`);
|
||||
|
||||
// 1. Decode the naddr
|
||||
// Validate naddr format
|
||||
if (!naddr.startsWith('naddr1')) {
|
||||
throw new Error('Invalid naddr format. Should start with "naddr1"');
|
||||
}
|
||||
|
||||
// Decode naddr
|
||||
const decoded = nip19.decode(naddr);
|
||||
if (decoded.type !== 'naddr') {
|
||||
throw new Error('Invalid naddr format');
|
||||
}
|
||||
|
||||
const { pubkey, kind, identifier } = decoded.data as { pubkey: string, kind: number, identifier?: string };
|
||||
const { pubkey, kind, identifier } = decoded.data;
|
||||
console.log(`Decoded naddr: pubkey=${pubkey}, kind=${kind}, identifier=${identifier}`);
|
||||
|
||||
// 2. Check that it's a curation list (kind 30004)
|
||||
if (kind !== 30004) {
|
||||
throw new Error('Not a valid NIP-51 curation list');
|
||||
}
|
||||
|
||||
// 3. Create a filter to fetch the pack event
|
||||
// Create filter to fetch the pack event
|
||||
const packFilter: NDKFilter = {
|
||||
kinds: [kind],
|
||||
authors: [pubkey],
|
||||
@ -49,49 +57,47 @@ class POWRPackService {
|
||||
|
||||
console.log(`Fetching pack with filter: ${JSON.stringify(packFilter)}`);
|
||||
|
||||
// 4. Fetch the pack event
|
||||
const packEvents = await ndk.fetchEvents(packFilter);
|
||||
if (packEvents.size === 0) {
|
||||
// Fetch the pack event
|
||||
const events = await ndk.fetchEvents(packFilter);
|
||||
if (events.size === 0) {
|
||||
throw new Error('Pack not found');
|
||||
}
|
||||
|
||||
const packEvent = Array.from(packEvents)[0];
|
||||
// Get the first matching event
|
||||
const packEvent = Array.from(events)[0];
|
||||
console.log(`Fetched pack event: ${packEvent.id}`);
|
||||
|
||||
// Get tags for debugging
|
||||
console.log(`Pack tags: ${JSON.stringify(packEvent.tags)}`);
|
||||
|
||||
// 5. Extract template and exercise references
|
||||
// Extract template and exercise references
|
||||
const templateRefs: string[] = [];
|
||||
const exerciseRefs: string[] = [];
|
||||
|
||||
for (const tag of packEvent.tags) {
|
||||
if (tag[0] === 'a' && tag.length > 1) {
|
||||
const addressPointer = tag[1];
|
||||
// Use NDK's getMatchingTags for more reliable tag handling
|
||||
const aTags = packEvent.getMatchingTags('a');
|
||||
|
||||
// Format is kind:pubkey:d-tag
|
||||
for (const tag of aTags) {
|
||||
if (tag.length < 2) continue;
|
||||
|
||||
const addressPointer = tag[1];
|
||||
if (addressPointer.startsWith('33402:')) {
|
||||
// Workout template
|
||||
templateRefs.push(addressPointer);
|
||||
console.log(`Found template reference: ${addressPointer}`);
|
||||
templateRefs.push(addressPointer);
|
||||
} else if (addressPointer.startsWith('33401:')) {
|
||||
// Exercise
|
||||
exerciseRefs.push(addressPointer);
|
||||
console.log(`Found exercise reference: ${addressPointer}`);
|
||||
}
|
||||
exerciseRefs.push(addressPointer);
|
||||
}
|
||||
}
|
||||
|
||||
console.log(`Found ${templateRefs.length} template refs and ${exerciseRefs.length} exercise refs`);
|
||||
|
||||
// 6. Fetch templates and exercises
|
||||
console.log('Fetching referenced templates...');
|
||||
// Fetch referenced templates and exercises
|
||||
const templates = await this.fetchReferencedEvents(ndk, templateRefs);
|
||||
|
||||
console.log('Fetching referenced exercises...');
|
||||
const exercises = await this.fetchReferencedEvents(ndk, exerciseRefs);
|
||||
|
||||
console.log(`Fetched ${templates.length} templates and ${exercises.length} exercises`);
|
||||
|
||||
// 7. Return the complete pack data
|
||||
return {
|
||||
packEvent,
|
||||
templates,
|
||||
@ -104,76 +110,51 @@ class POWRPackService {
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper function to fetch events from address pointers
|
||||
* Fetch referenced events (templates or exercises)
|
||||
*/
|
||||
async fetchReferencedEvents(ndk: NDK, addressPointers: string[]): Promise<NDKEvent[]> {
|
||||
async fetchReferencedEvents(ndk: NDK, refs: string[]): Promise<NDKEvent[]> {
|
||||
if (refs.length === 0) return [];
|
||||
|
||||
console.log(`Fetching references: ${JSON.stringify(refs)}`);
|
||||
|
||||
const events: NDKEvent[] = [];
|
||||
|
||||
console.log("Fetching references:", addressPointers);
|
||||
|
||||
for (const pointer of addressPointers) {
|
||||
for (const ref of refs) {
|
||||
try {
|
||||
// Parse the pointer (kind:pubkey:d-tag)
|
||||
const parts = pointer.split(':');
|
||||
if (parts.length < 3) {
|
||||
console.error(`Invalid address pointer format: ${pointer}`);
|
||||
continue;
|
||||
}
|
||||
|
||||
// Extract the components
|
||||
const kindStr = parts[0];
|
||||
const hexPubkey = parts[1];
|
||||
const dTagOrEventId = parts[2];
|
||||
|
||||
// Parse the reference format (kind:pubkey:d-tag)
|
||||
const [kindStr, pubkey, dTag] = ref.split(':');
|
||||
const kind = parseInt(kindStr);
|
||||
if (isNaN(kind)) {
|
||||
console.error(`Invalid kind in pointer: ${kindStr}`);
|
||||
continue;
|
||||
}
|
||||
|
||||
console.log(`Fetching ${kind} event with d-tag ${dTagOrEventId} from author ${hexPubkey}`);
|
||||
console.log(`Fetching ${kind} event with d-tag ${dTag} from author ${pubkey}`);
|
||||
|
||||
// Try direct event ID fetching first
|
||||
try {
|
||||
console.log(`Trying to fetch event directly by ID: ${dTagOrEventId}`);
|
||||
const directEvent = await ndk.fetchEvent({ids: [dTagOrEventId]});
|
||||
if (directEvent) {
|
||||
console.log(`Successfully fetched event by ID: ${dTagOrEventId}`);
|
||||
events.push(directEvent);
|
||||
continue; // Skip to next loop iteration
|
||||
}
|
||||
} catch (directFetchError) {
|
||||
console.log(`Direct fetch failed, falling back to filters: ${directFetchError}`);
|
||||
}
|
||||
|
||||
// Create a filter as fallback
|
||||
// Create a filter to find this specific event
|
||||
const filter: NDKFilter = {
|
||||
kinds: [kind],
|
||||
authors: [hexPubkey],
|
||||
authors: [pubkey],
|
||||
'#d': [dTag]
|
||||
};
|
||||
|
||||
if (dTagOrEventId && dTagOrEventId.length > 0) {
|
||||
// For parameterized replaceable events, use d-tag
|
||||
filter['#d'] = [dTagOrEventId];
|
||||
}
|
||||
|
||||
console.log("Using filter:", JSON.stringify(filter));
|
||||
|
||||
// Fetch the events with a timeout
|
||||
const fetchPromise = ndk.fetchEvents(filter);
|
||||
const timeoutPromise = new Promise<Set<NDKEvent>>((_, reject) =>
|
||||
setTimeout(() => reject(new Error('Fetch timeout')), 10000)
|
||||
);
|
||||
|
||||
const fetchedEvents = await Promise.race([fetchPromise, timeoutPromise]);
|
||||
console.log(`Found ${fetchedEvents.size} events for ${pointer}`);
|
||||
// Try to fetch by filter first
|
||||
const fetchedEvents = await ndk.fetchEvents(filter);
|
||||
|
||||
if (fetchedEvents.size > 0) {
|
||||
events.push(...Array.from(fetchedEvents));
|
||||
events.push(Array.from(fetchedEvents)[0]);
|
||||
continue;
|
||||
}
|
||||
|
||||
// If not found by d-tag, try to fetch by ID directly
|
||||
console.log(`Trying to fetch event directly by ID: ${dTag}`);
|
||||
try {
|
||||
const event = await ndk.fetchEvent(dTag);
|
||||
if (event) {
|
||||
console.log(`Successfully fetched event by ID: ${dTag}`);
|
||||
events.push(event);
|
||||
}
|
||||
} catch (idError) {
|
||||
console.error(`Error fetching by ID: ${idError}`);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(`Error fetching event with pointer ${pointer}:`, error);
|
||||
// Continue with other events even if one fails
|
||||
console.error(`Error fetching reference ${ref}:`, error);
|
||||
}
|
||||
}
|
||||
|
||||
@ -182,135 +163,121 @@ class POWRPackService {
|
||||
}
|
||||
|
||||
/**
|
||||
* Analyzes templates and identifies their exercise dependencies
|
||||
* Analyze dependencies between templates and exercises
|
||||
*/
|
||||
analyzeDependencies(templates: NDKEvent[], exercises: NDKEvent[]): Record<string, string[]> {
|
||||
const dependencies: Record<string, string[]> = {};
|
||||
const exerciseMap: Record<string, string> = {};
|
||||
const exerciseMap = new Map<string, NDKEvent>();
|
||||
|
||||
console.log(`Analyzing dependencies for ${templates.length} templates and ${exercises.length} exercises`);
|
||||
|
||||
// Create lookup map for exercises by reference
|
||||
exercises.forEach(exercise => {
|
||||
const dTag = findTagValue(exercise.tags, 'd');
|
||||
// Map exercises by "kind:pubkey:d-tag" for easier lookup
|
||||
for (const exercise of exercises) {
|
||||
const dTag = exercise.tagValue('d');
|
||||
if (dTag) {
|
||||
const exerciseRef = `33401:${exercise.pubkey}:${dTag}`;
|
||||
exerciseMap[exerciseRef] = exercise.id;
|
||||
console.log(`Mapped exercise ${exercise.id} to reference ${exerciseRef}`);
|
||||
const reference = `33401:${exercise.pubkey}:${dTag}`;
|
||||
exerciseMap.set(reference, exercise);
|
||||
console.log(`Mapped exercise ${exercise.id} to reference ${reference}`);
|
||||
}
|
||||
}
|
||||
|
||||
// Analyze each template for its exercise dependencies
|
||||
for (const template of templates) {
|
||||
const templateId = template.id;
|
||||
const templateName = template.tagValue('title') || 'Unnamed Template';
|
||||
|
||||
console.log(`Analyzing template ${templateName} (${templateId})`);
|
||||
dependencies[templateId] = [];
|
||||
|
||||
// Get exercise references from template
|
||||
const exerciseTags = template.getMatchingTags('exercise');
|
||||
|
||||
for (const tag of exerciseTags) {
|
||||
if (tag.length < 2) continue;
|
||||
|
||||
const exerciseRef = tag[1];
|
||||
console.log(`Template ${templateName} references ${exerciseRef}`);
|
||||
|
||||
// Find the exercise in our mapped exercises
|
||||
const exercise = exerciseMap.get(exerciseRef);
|
||||
if (exercise) {
|
||||
dependencies[templateId].push(exercise.id);
|
||||
console.log(`Template ${templateName} depends on exercise ${exercise.id}`);
|
||||
} else {
|
||||
console.log(`Exercise ${exercise.id} has no d-tag`);
|
||||
}
|
||||
});
|
||||
|
||||
// Analyze each template for exercise references
|
||||
templates.forEach(template => {
|
||||
const requiredExercises: string[] = [];
|
||||
const templateName = findTagValue(template.tags, 'title') || template.id.substring(0, 8);
|
||||
|
||||
console.log(`Analyzing template ${templateName} (${template.id})`);
|
||||
|
||||
// Find exercise references in template tags
|
||||
template.tags.forEach(tag => {
|
||||
if (tag[0] === 'exercise' && tag.length > 1) {
|
||||
const exerciseRefFull = tag[1];
|
||||
|
||||
// Split the reference to get the base part (without parameters)
|
||||
const refParts = exerciseRefFull.split('::');
|
||||
const baseRef = refParts[0];
|
||||
|
||||
const exerciseId = exerciseMap[baseRef];
|
||||
|
||||
if (exerciseId) {
|
||||
requiredExercises.push(exerciseId);
|
||||
console.log(`Template ${templateName} requires exercise ${exerciseId} via ref ${baseRef}`);
|
||||
} else {
|
||||
console.log(`Template ${templateName} references unknown exercise ${exerciseRefFull}`);
|
||||
console.log(`Template ${templateName} references unknown exercise ${exerciseRef}`);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
dependencies[template.id] = requiredExercises;
|
||||
console.log(`Template ${templateName} has ${requiredExercises.length} dependencies`);
|
||||
});
|
||||
console.log(`Template ${templateName} has ${dependencies[templateId].length} dependencies`);
|
||||
}
|
||||
|
||||
return dependencies;
|
||||
}
|
||||
|
||||
/**
|
||||
* Import a POWR Pack and selected items into the database
|
||||
* Import a POWR Pack into the local database
|
||||
*/
|
||||
async importPack(
|
||||
packImport: POWRPackImport,
|
||||
selection: POWRPackSelection
|
||||
): Promise<string> {
|
||||
async importPack(packImport: POWRPackImport, selection: POWRPackSelection): Promise<void> {
|
||||
try {
|
||||
const { packEvent, templates, exercises } = packImport;
|
||||
const { selectedTemplates, selectedExercises } = selection;
|
||||
console.log(`Importing ${selection.selectedExercises.length} exercises...`);
|
||||
|
||||
// Create integration helper
|
||||
const nostrIntegration = new NostrIntegration(this.db);
|
||||
// Map to track imported exercise IDs by various reference formats
|
||||
const exerciseIdMap = new Map<string, string>();
|
||||
|
||||
// 1. Extract pack metadata
|
||||
const title = findTagValue(packEvent.tags, 'name') || 'Unnamed Pack';
|
||||
const description = findTagValue(packEvent.tags, 'about') || packEvent.content;
|
||||
// First, import the selected exercises
|
||||
for (const exerciseId of selection.selectedExercises) {
|
||||
const exerciseEvent = packImport.exercises.find(e => e.id === exerciseId);
|
||||
if (!exerciseEvent) continue;
|
||||
|
||||
// 2. Create pack record
|
||||
const packId = generateId();
|
||||
const now = Date.now();
|
||||
// Get the d-tag value from the event
|
||||
const dTag = exerciseEvent.tagValue('d');
|
||||
|
||||
await this.db.withTransactionAsync(async () => {
|
||||
// Insert pack record
|
||||
await this.db.runAsync(
|
||||
`INSERT INTO powr_packs (id, title, description, author_pubkey, nostr_event_id, import_date, updated_at)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?)`,
|
||||
[packId, title, description, packEvent.pubkey, packEvent.id, now, now]
|
||||
);
|
||||
|
||||
// 3. Process and import selected exercises
|
||||
const exercisesToImport = exercises.filter((e: NDKEvent) => selectedExercises.includes(e.id));
|
||||
const importedExerciseIds: string[] = [];
|
||||
const exerciseIdMap = new Map<string, string>(); // Map Nostr event ID to local ID
|
||||
|
||||
console.log(`Importing ${exercisesToImport.length} exercises...`);
|
||||
|
||||
for (const exerciseEvent of exercisesToImport) {
|
||||
// Convert to local model
|
||||
const exercise = nostrIntegration.convertNostrExerciseToLocal(exerciseEvent);
|
||||
const exerciseModel = this.nostrIntegration.convertNostrExerciseToLocal(exerciseEvent);
|
||||
|
||||
// Save to database
|
||||
await nostrIntegration.saveImportedExercise(exercise);
|
||||
const localId = await this.nostrIntegration.saveImportedExercise(exerciseModel, exerciseEvent);
|
||||
|
||||
// Track imported exercise
|
||||
importedExerciseIds.push(exercise.id);
|
||||
exerciseIdMap.set(exerciseEvent.id, exercise.id);
|
||||
// Map ALL possible ways to reference this exercise:
|
||||
|
||||
console.log(`Imported exercise: ${exercise.title} (${exercise.id}) from Nostr event ${exerciseEvent.id}`);
|
||||
// 1. By event ID directly (fallback)
|
||||
exerciseIdMap.set(exerciseId, localId);
|
||||
|
||||
// Create pack item record
|
||||
await this.createPackItemRecord(packId, exercise.id, 'exercise', exerciseEvent.id);
|
||||
// 2. By standard d-tag reference format (if d-tag exists)
|
||||
if (dTag) {
|
||||
const dTagRef = `33401:${exerciseEvent.pubkey}:${dTag}`;
|
||||
exerciseIdMap.set(dTagRef, localId);
|
||||
console.log(`Mapped d-tag reference ${dTagRef} to local exercise ID ${localId}`);
|
||||
}
|
||||
|
||||
// 4. Process and import selected templates
|
||||
const templatesToImport = templates.filter((t: NDKEvent) => selectedTemplates.includes(t.id));
|
||||
// 3. By event ID as d-tag (for templates that reference this way)
|
||||
const eventIdRef = `33401:${exerciseEvent.pubkey}:${exerciseId}`;
|
||||
exerciseIdMap.set(eventIdRef, localId);
|
||||
console.log(`Mapped event ID reference ${eventIdRef} to local exercise ID ${localId}`);
|
||||
|
||||
console.log(`Importing ${templatesToImport.length} templates...`);
|
||||
console.log(`Imported exercise: ${exerciseModel.title} (${localId}) from Nostr event ${exerciseId}`);
|
||||
}
|
||||
|
||||
console.log(`Importing ${selection.selectedTemplates.length} templates...`);
|
||||
|
||||
// Then, import the selected templates
|
||||
for (const templateId of selection.selectedTemplates) {
|
||||
const templateEvent = packImport.templates.find(t => t.id === templateId);
|
||||
if (!templateEvent) continue;
|
||||
|
||||
for (const templateEvent of templatesToImport) {
|
||||
// Convert to local model
|
||||
const templateModel = nostrIntegration.convertNostrTemplateToLocal(templateEvent);
|
||||
const templateModel = this.nostrIntegration.convertNostrTemplateToLocal(templateEvent);
|
||||
|
||||
// Save to database
|
||||
await nostrIntegration.saveImportedTemplate(templateModel);
|
||||
const localTemplateId = await this.nostrIntegration.saveImportedTemplate(templateModel, templateEvent);
|
||||
|
||||
console.log(`Imported template: ${templateModel.title} (${templateModel.id}) from Nostr event ${templateEvent.id}`);
|
||||
console.log(`Imported template: ${templateModel.title} (${localTemplateId}) from Nostr event ${templateId}`);
|
||||
|
||||
// Get exercise references from this template
|
||||
const exerciseRefs = nostrIntegration.getTemplateExerciseRefs(templateEvent);
|
||||
const exerciseRefs = this.nostrIntegration.getTemplateExerciseRefs(templateEvent);
|
||||
|
||||
console.log(`Template has ${exerciseRefs.length} exercise references:`);
|
||||
exerciseRefs.forEach(ref => console.log(` - ${ref}`));
|
||||
|
||||
// Find the corresponding imported exercise IDs
|
||||
// Map exercise references to local exercise IDs
|
||||
const templateExerciseIds: string[] = [];
|
||||
const matchedRefs: string[] = [];
|
||||
|
||||
@ -321,207 +288,378 @@ class POWRPackService {
|
||||
|
||||
console.log(`Looking for matching exercise for reference: ${baseRef}`);
|
||||
|
||||
// Find the event that matches this reference
|
||||
const matchingEvent = exercises.find(e => {
|
||||
const dTag = findTagValue(e.tags, 'd');
|
||||
if (!dTag) return false;
|
||||
// Check if we have this reference in our map
|
||||
if (exerciseIdMap.has(baseRef)) {
|
||||
const localExerciseId = exerciseIdMap.get(baseRef) || '';
|
||||
templateExerciseIds.push(localExerciseId);
|
||||
matchedRefs.push(ref);
|
||||
|
||||
const fullRef = `33401:${e.pubkey}:${dTag}`;
|
||||
const match = baseRef === fullRef;
|
||||
|
||||
if (match) {
|
||||
console.log(`Found matching event: ${e.id} with d-tag: ${dTag}`);
|
||||
console.log(`Mapped reference ${baseRef} to local exercise ID ${localExerciseId}`);
|
||||
continue;
|
||||
}
|
||||
|
||||
return match;
|
||||
});
|
||||
// If not found by direct reference, try to match by examining individual components
|
||||
console.log(`No direct match for reference: ${baseRef}. Trying to match by components...`);
|
||||
|
||||
// Parse the reference for fallback matching
|
||||
const refSegments = baseRef.split(':');
|
||||
if (refSegments.length >= 3) {
|
||||
const refKind = refSegments[0];
|
||||
const refPubkey = refSegments[1];
|
||||
const refDTag = refSegments[2];
|
||||
|
||||
// Try to find the matching exercise by looking at both event ID and d-tag
|
||||
for (const [key, value] of exerciseIdMap.entries()) {
|
||||
// Check if this is potentially the same exercise with a different reference format
|
||||
if (key.includes(refPubkey) && (key.includes(refDTag) || key.endsWith(refDTag))) {
|
||||
templateExerciseIds.push(value);
|
||||
matchedRefs.push(ref);
|
||||
|
||||
// Also add this reference format to map for future lookups
|
||||
exerciseIdMap.set(baseRef, value);
|
||||
|
||||
console.log(`Found potential match using partial comparison: ${key} -> ${value}`);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// If no match found yet, check if there's a direct event ID match
|
||||
if (templateExerciseIds.length === templateExerciseIds.lastIndexOf(refDTag) + 1) {
|
||||
// Didn't add anything in the above loop, try direct event ID lookup
|
||||
const matchingEvent = packImport.exercises.find(e => e.id === refDTag);
|
||||
|
||||
if (matchingEvent && exerciseIdMap.has(matchingEvent.id)) {
|
||||
const localExerciseId = exerciseIdMap.get(matchingEvent.id) || '';
|
||||
templateExerciseIds.push(localExerciseId);
|
||||
matchedRefs.push(ref); // Keep the full reference including parameters
|
||||
matchedRefs.push(ref);
|
||||
|
||||
console.log(`Mapped Nostr event ${matchingEvent.id} to local exercise ID ${localExerciseId}`);
|
||||
// Add this reference to our map for future use
|
||||
exerciseIdMap.set(baseRef, localExerciseId);
|
||||
|
||||
console.log(`Found match by event ID: ${matchingEvent.id} -> ${localExerciseId}`);
|
||||
} else {
|
||||
console.log(`No matching exercise found for reference: ${baseRef}`);
|
||||
console.log(`No matching exercise found for reference components: kind=${refKind}, pubkey=${refPubkey}, d-tag=${refDTag}`);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
console.log(`Invalid reference format: ${baseRef}`);
|
||||
}
|
||||
}
|
||||
|
||||
// Save template-exercise relationships with parameters
|
||||
if (templateExerciseIds.length > 0) {
|
||||
await nostrIntegration.saveTemplateExercisesWithParams(templateModel.id, templateExerciseIds, matchedRefs);
|
||||
await this.nostrIntegration.saveTemplateExercisesWithParams(
|
||||
localTemplateId,
|
||||
templateExerciseIds,
|
||||
matchedRefs
|
||||
);
|
||||
|
||||
// Log the result
|
||||
console.log(`Checking saved template: ${localTemplateId}`);
|
||||
const templateExercises = await this.db.getAllAsync<{ exercise_id: string }>(
|
||||
`SELECT exercise_id FROM template_exercises WHERE template_id = ?`,
|
||||
[localTemplateId]
|
||||
);
|
||||
console.log(`Template ${templateModel.title} has ${templateExercises.length} exercises associated`);
|
||||
} else {
|
||||
console.log(`No exercise relationships to save for template ${templateModel.id}`);
|
||||
console.log(`No exercise relationships to save for template ${localTemplateId}`);
|
||||
}
|
||||
}
|
||||
|
||||
// Create pack item record
|
||||
await this.createPackItemRecord(packId, templateModel.id, 'template', templateEvent.id);
|
||||
// Finally, save the pack itself
|
||||
await this.savePack(packImport.packEvent, selection);
|
||||
|
||||
// Add diagnostic logging
|
||||
console.log(`Checking saved template: ${templateModel.id}`);
|
||||
const exerciseCount = await this.db.getFirstAsync<{count: number}>(
|
||||
'SELECT COUNT(*) as count FROM template_exercises WHERE template_id = ?',
|
||||
[templateModel.id]
|
||||
// Get total counts
|
||||
const totalNostrTemplates = await this.db.getFirstAsync<{ count: number }>(
|
||||
`SELECT COUNT(*) as count FROM templates WHERE source = 'nostr'`
|
||||
);
|
||||
console.log(`Template ${templateModel.title} has ${exerciseCount?.count || 0} exercises associated`);
|
||||
}
|
||||
|
||||
// Final diagnostic check
|
||||
const templateCount = await this.db.getFirstAsync<{count: number}>(
|
||||
'SELECT COUNT(*) as count FROM templates WHERE source = "nostr"'
|
||||
);
|
||||
console.log(`Total nostr templates in database: ${templateCount?.count || 0}`);
|
||||
console.log(`Total nostr templates in database: ${totalNostrTemplates?.count || 0}`);
|
||||
|
||||
const templateIds = await this.db.getAllAsync<{id: string, title: string}>(
|
||||
'SELECT id, title FROM templates WHERE source = "nostr"'
|
||||
// Get imported template IDs for verification
|
||||
const templates = await this.db.getAllAsync<{ id: string, title: string }>(
|
||||
`SELECT id, title FROM templates WHERE source = 'nostr'`
|
||||
);
|
||||
|
||||
console.log(`Template IDs:`);
|
||||
templateIds.forEach(t => console.log(` - ${t.title}: ${t.id}`));
|
||||
templates.forEach(t => {
|
||||
console.log(` - ${t.title}: ${t.id}`);
|
||||
});
|
||||
|
||||
return packId;
|
||||
} catch (error) {
|
||||
console.error('Error importing POWR pack:', error);
|
||||
console.error('Error importing pack:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a record of a pack item
|
||||
* Save the pack metadata to the database
|
||||
*/
|
||||
private async createPackItemRecord(
|
||||
packId: string,
|
||||
itemId: string,
|
||||
itemType: 'exercise' | 'template',
|
||||
nostrEventId?: string,
|
||||
itemOrder?: number
|
||||
): Promise<void> {
|
||||
private async savePack(packEvent: NDKEvent, selection: POWRPackSelection): Promise<string> {
|
||||
try {
|
||||
const now = Date.now();
|
||||
|
||||
// Get pack metadata
|
||||
const title = packEvent.tagValue('name') || 'Unnamed Pack';
|
||||
const description = packEvent.tagValue('about') || packEvent.content || '';
|
||||
|
||||
// Save pack to database
|
||||
await this.db.runAsync(
|
||||
`INSERT INTO powr_packs (id, title, description, author_pubkey, nostr_event_id, import_date, updated_at)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?)`,
|
||||
[
|
||||
selection.packId,
|
||||
title,
|
||||
description,
|
||||
packEvent.pubkey,
|
||||
packEvent.id,
|
||||
now,
|
||||
now
|
||||
]
|
||||
);
|
||||
|
||||
// Save pack items (templates and exercises)
|
||||
let order = 0;
|
||||
|
||||
// Save template items
|
||||
for (const templateId of selection.selectedTemplates) {
|
||||
await this.db.runAsync(
|
||||
`INSERT INTO powr_pack_items (pack_id, item_id, item_type, item_order, is_imported, nostr_event_id)
|
||||
VALUES (?, ?, ?, ?, ?, ?)`,
|
||||
[packId, itemId, itemType, itemOrder || 0, 1, nostrEventId || null]
|
||||
[
|
||||
selection.packId,
|
||||
templateId,
|
||||
'template',
|
||||
order++,
|
||||
1, // Imported
|
||||
templateId
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
// Save exercise items
|
||||
for (const exerciseId of selection.selectedExercises) {
|
||||
await this.db.runAsync(
|
||||
`INSERT INTO powr_pack_items (pack_id, item_id, item_type, item_order, is_imported, nostr_event_id)
|
||||
VALUES (?, ?, ?, ?, ?, ?)`,
|
||||
[
|
||||
selection.packId,
|
||||
exerciseId,
|
||||
'exercise',
|
||||
order++,
|
||||
1, // Imported
|
||||
exerciseId
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
return selection.packId;
|
||||
} catch (error) {
|
||||
console.error('Error saving pack:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all imported packs
|
||||
*/
|
||||
async getImportedPacks(): Promise<POWRPackWithContent[]> {
|
||||
try {
|
||||
// 1. Get all packs
|
||||
const packs = await this.db.getAllAsync<POWRPack>(
|
||||
`SELECT id, title, description, author_pubkey as authorPubkey,
|
||||
nostr_event_id as nostrEventId, import_date as importDate, updated_at as updatedAt
|
||||
FROM powr_packs
|
||||
ORDER BY import_date DESC`
|
||||
// Get all packs
|
||||
const packs = await this.db.getAllAsync<{
|
||||
id: string;
|
||||
title: string;
|
||||
description: string;
|
||||
author_pubkey: string;
|
||||
nostr_event_id: string;
|
||||
import_date: number;
|
||||
updated_at: number;
|
||||
}>(
|
||||
`SELECT * FROM powr_packs ORDER BY import_date DESC`
|
||||
);
|
||||
|
||||
// 2. Get content for each pack
|
||||
// For each pack, get its templates and exercises
|
||||
const result: POWRPackWithContent[] = [];
|
||||
|
||||
for (const pack of packs) {
|
||||
// Get exercises
|
||||
const exercises = await this.db.getAllAsync<BaseExercise>(
|
||||
`SELECT e.*
|
||||
FROM exercises e
|
||||
JOIN powr_pack_items ppi ON e.id = ppi.item_id
|
||||
WHERE ppi.pack_id = ? AND ppi.item_type = 'exercise' AND ppi.is_imported = 1`,
|
||||
for (const dbPack of packs) {
|
||||
// Transform to match POWRPack type
|
||||
const pack: POWRPack = {
|
||||
id: dbPack.id,
|
||||
title: dbPack.title,
|
||||
description: dbPack.description || '',
|
||||
authorPubkey: dbPack.author_pubkey,
|
||||
nostrEventId: dbPack.nostr_event_id,
|
||||
importDate: dbPack.import_date,
|
||||
updatedAt: dbPack.updated_at
|
||||
};
|
||||
|
||||
// Get templates
|
||||
const templateData = await this.db.getAllAsync<{
|
||||
id: string;
|
||||
title: string;
|
||||
type: string;
|
||||
description: string;
|
||||
created_at: number;
|
||||
}>(
|
||||
`SELECT t.id, t.title, t.type, t.description, t.created_at
|
||||
FROM templates t
|
||||
JOIN powr_pack_items ppi ON ppi.item_id = t.nostr_event_id
|
||||
WHERE ppi.pack_id = ? AND ppi.item_type = 'template'
|
||||
ORDER BY ppi.item_order`,
|
||||
[pack.id]
|
||||
);
|
||||
|
||||
// Get templates
|
||||
const templates = await this.db.getAllAsync<WorkoutTemplate>(
|
||||
`SELECT t.*
|
||||
FROM templates t
|
||||
JOIN powr_pack_items ppi ON t.id = ppi.item_id
|
||||
WHERE ppi.pack_id = ? AND ppi.item_type = 'template' AND ppi.is_imported = 1`,
|
||||
// Transform template data to match WorkoutTemplate type
|
||||
const templates: WorkoutTemplate[] = templateData.map(t => ({
|
||||
id: t.id,
|
||||
title: t.title,
|
||||
type: (t.type || 'strength') as TemplateType,
|
||||
category: 'Custom', // Default value
|
||||
description: t.description,
|
||||
exercises: [], // Default empty array
|
||||
isPublic: true, // Default value
|
||||
version: 1, // Default value
|
||||
tags: [], // Default empty array
|
||||
created_at: t.created_at,
|
||||
availability: {
|
||||
source: ['nostr']
|
||||
}
|
||||
}));
|
||||
|
||||
// Get exercises
|
||||
const exerciseData = await this.db.getAllAsync<{
|
||||
id: string;
|
||||
title: string;
|
||||
type: string;
|
||||
category: string;
|
||||
description: string;
|
||||
created_at: number;
|
||||
}>(
|
||||
`SELECT e.id, e.title, e.type, e.category, e.description, e.created_at
|
||||
FROM exercises e
|
||||
JOIN powr_pack_items ppi ON ppi.item_id = e.nostr_event_id
|
||||
WHERE ppi.pack_id = ? AND ppi.item_type = 'exercise'
|
||||
ORDER BY ppi.item_order`,
|
||||
[pack.id]
|
||||
);
|
||||
|
||||
// Transform exercise data to match BaseExercise type
|
||||
const exercises: BaseExercise[] = exerciseData.map(e => ({
|
||||
id: e.id,
|
||||
title: e.title,
|
||||
type: e.type as ExerciseType,
|
||||
category: e.category as ExerciseCategory,
|
||||
description: e.description,
|
||||
tags: [], // Default empty array
|
||||
format: {}, // Default empty object
|
||||
format_units: {}, // Default empty object
|
||||
created_at: e.created_at,
|
||||
availability: {
|
||||
source: ['nostr']
|
||||
}
|
||||
}));
|
||||
|
||||
result.push({
|
||||
pack,
|
||||
exercises,
|
||||
templates
|
||||
templates,
|
||||
exercises
|
||||
});
|
||||
}
|
||||
|
||||
return result;
|
||||
} catch (error) {
|
||||
console.error('Error getting imported packs:', error);
|
||||
throw error;
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a specific pack by ID
|
||||
* Create a shareable naddr for a POWR Pack
|
||||
* @param packEvent The Nostr event for the pack
|
||||
* @returns A shareable naddr string
|
||||
*/
|
||||
async getPackById(packId: string): Promise<POWRPackWithContent | null> {
|
||||
createShareableNaddr(packEvent: NDKEvent): string {
|
||||
try {
|
||||
// 1. Get pack info
|
||||
const pack = await this.db.getFirstAsync<POWRPack>(
|
||||
`SELECT id, title, description, author_pubkey as authorPubkey,
|
||||
nostr_event_id as nostrEventId, import_date as importDate, updated_at as updatedAt
|
||||
FROM powr_packs
|
||||
WHERE id = ?`,
|
||||
[packId]
|
||||
);
|
||||
// Extract d-tag for the pack (required for naddr)
|
||||
const dTag = packEvent.tagValue('d');
|
||||
|
||||
if (!pack) {
|
||||
return null;
|
||||
if (!dTag) {
|
||||
throw new Error('Pack event missing required d-tag');
|
||||
}
|
||||
|
||||
// 2. Get exercises
|
||||
const exercises = await this.db.getAllAsync<BaseExercise>(
|
||||
`SELECT e.*
|
||||
FROM exercises e
|
||||
JOIN powr_pack_items ppi ON e.id = ppi.item_id
|
||||
WHERE ppi.pack_id = ? AND ppi.item_type = 'exercise' AND ppi.is_imported = 1`,
|
||||
[packId]
|
||||
);
|
||||
|
||||
// 3. Get templates
|
||||
const templates = await this.db.getAllAsync<WorkoutTemplate>(
|
||||
`SELECT t.*
|
||||
FROM templates t
|
||||
JOIN powr_pack_items ppi ON t.id = ppi.item_id
|
||||
WHERE ppi.pack_id = ? AND ppi.item_type = 'template' AND ppi.is_imported = 1`,
|
||||
[packId]
|
||||
);
|
||||
|
||||
return {
|
||||
pack,
|
||||
exercises,
|
||||
templates
|
||||
};
|
||||
// Create naddr using NDK's methods
|
||||
const naddr = packEvent.encode();
|
||||
return naddr;
|
||||
} catch (error) {
|
||||
console.error('Error getting pack by ID:', error);
|
||||
throw error;
|
||||
console.error('Error creating shareable naddr:', error);
|
||||
|
||||
// Fallback: manually construct naddr if NDK's encode fails
|
||||
try {
|
||||
const { nip19 } = require('nostr-tools');
|
||||
|
||||
const dTag = packEvent.tagValue('d') || '';
|
||||
|
||||
return nip19.naddrEncode({
|
||||
kind: packEvent.kind,
|
||||
pubkey: packEvent.pubkey,
|
||||
identifier: dTag,
|
||||
relays: [] // Optional relay hints
|
||||
});
|
||||
} catch (fallbackError) {
|
||||
console.error('Fallback naddr creation failed:', fallbackError);
|
||||
throw new Error('Could not create shareable link for pack');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete a pack and optionally its contents
|
||||
* Delete a POWR Pack
|
||||
* @param packId The ID of the pack to delete
|
||||
* @param keepItems Whether to keep the imported templates and exercises
|
||||
*/
|
||||
async deletePack(packId: string, keepItems: boolean = false): Promise<void> {
|
||||
async deletePack(packId: string, keepItems: boolean = true): Promise<void> {
|
||||
try {
|
||||
await this.db.withTransactionAsync(async () => {
|
||||
if (!keepItems) {
|
||||
// Get the items first so we can delete them from their respective tables
|
||||
const items = await this.db.getAllAsync<POWRPackItem>(
|
||||
`SELECT * FROM powr_pack_items WHERE pack_id = ? AND is_imported = 1`,
|
||||
// Get all templates and exercises from this pack
|
||||
const templates = await this.db.getAllAsync<{ id: string }>(
|
||||
`SELECT t.id
|
||||
FROM templates t
|
||||
JOIN powr_pack_items ppi ON ppi.item_id = t.nostr_event_id
|
||||
WHERE ppi.pack_id = ? AND ppi.item_type = 'template'`,
|
||||
[packId]
|
||||
);
|
||||
|
||||
// Delete each exercise and template
|
||||
for (const item of items as POWRPackItem[]) {
|
||||
if (item.itemType === 'exercise') {
|
||||
// Delete exercise
|
||||
await this.db.runAsync(`DELETE FROM exercises WHERE id = ?`, [item.itemId]);
|
||||
} else if (item.itemType === 'template') {
|
||||
// Delete template and its relationships
|
||||
await this.db.runAsync(`DELETE FROM template_exercises WHERE template_id = ?`, [item.itemId]);
|
||||
await this.db.runAsync(`DELETE FROM templates WHERE id = ?`, [item.itemId]);
|
||||
const exercises = await this.db.getAllAsync<{ id: string }>(
|
||||
`SELECT e.id
|
||||
FROM exercises e
|
||||
JOIN powr_pack_items ppi ON ppi.item_id = e.nostr_event_id
|
||||
WHERE ppi.pack_id = ? AND ppi.item_type = 'exercise'`,
|
||||
[packId]
|
||||
);
|
||||
|
||||
// Delete the templates
|
||||
for (const template of templates) {
|
||||
await this.db.runAsync(
|
||||
`DELETE FROM template_exercises WHERE template_id = ?`,
|
||||
[template.id]
|
||||
);
|
||||
|
||||
await this.db.runAsync(
|
||||
`DELETE FROM templates WHERE id = ?`,
|
||||
[template.id]
|
||||
);
|
||||
}
|
||||
|
||||
// Delete the exercises
|
||||
for (const exercise of exercises) {
|
||||
await this.db.runAsync(
|
||||
`DELETE FROM exercise_tags WHERE exercise_id = ?`,
|
||||
[exercise.id]
|
||||
);
|
||||
|
||||
await this.db.runAsync(
|
||||
`DELETE FROM exercises WHERE id = ?`,
|
||||
[exercise.id]
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@ -531,43 +669,14 @@ class POWRPackService {
|
||||
[packId]
|
||||
);
|
||||
|
||||
// Delete the pack
|
||||
// Finally, delete the pack itself
|
||||
await this.db.runAsync(
|
||||
`DELETE FROM powr_packs WHERE id = ?`,
|
||||
[packId]
|
||||
);
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Error deleting pack:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Create an naddr for sharing a pack
|
||||
*/
|
||||
createShareableNaddr(packEvent: NDKEvent): string {
|
||||
try {
|
||||
// Extract the d-tag (identifier)
|
||||
const dTags = packEvent.getMatchingTags('d');
|
||||
const identifier = dTags[0]?.[1] || '';
|
||||
|
||||
// Ensure kind is a definite number (use 30004 as default if undefined)
|
||||
const kind = packEvent.kind !== undefined ? packEvent.kind : 30004;
|
||||
|
||||
// Create the naddr
|
||||
const naddr = nip19.naddrEncode({
|
||||
pubkey: packEvent.pubkey,
|
||||
kind: kind, // Now this is always a number
|
||||
identifier
|
||||
});
|
||||
|
||||
return naddr;
|
||||
} catch (error) {
|
||||
console.error('Error creating shareable naddr:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default POWRPackService;
|
@ -7,12 +7,26 @@ import {
|
||||
import { Workout } from '@/types/workout';
|
||||
import { generateId } from '@/utils/ids';
|
||||
import { DbService } from '../db-service';
|
||||
import { ExerciseService } from './ExerciseService';
|
||||
import { BaseExercise, ExerciseDisplay } from '@/types/exercise';
|
||||
|
||||
interface TemplateExerciseWithData {
|
||||
id: string;
|
||||
exercise: BaseExercise | ExerciseDisplay;
|
||||
displayOrder: number;
|
||||
targetSets?: number; // Changed from number | null to number | undefined
|
||||
targetReps?: number; // Changed from number | null to number | undefined
|
||||
targetWeight?: number; // Changed from number | null to number | undefined
|
||||
notes?: string;
|
||||
nostrReference?: string;
|
||||
}
|
||||
|
||||
export class TemplateService {
|
||||
private db: DbService;
|
||||
|
||||
constructor(database: SQLiteDatabase) {
|
||||
this.db = new DbService(database);
|
||||
constructor(db: SQLiteDatabase, private exerciseService: ExerciseService) {
|
||||
// Convert SQLiteDatabase to DbService
|
||||
this.db = db as unknown as DbService;
|
||||
}
|
||||
|
||||
/**
|
||||
@ -37,8 +51,10 @@ export class TemplateService {
|
||||
nostr_event_id: string | null;
|
||||
source: string;
|
||||
parent_id: string | null;
|
||||
author_pubkey: string | null;
|
||||
is_archived: number;
|
||||
}>(
|
||||
`SELECT * FROM templates ORDER BY updated_at DESC LIMIT ? OFFSET ?`,
|
||||
`SELECT * FROM templates WHERE is_archived = 0 ORDER BY updated_at DESC LIMIT ? OFFSET ?`,
|
||||
[limit, offset]
|
||||
);
|
||||
|
||||
@ -57,11 +73,13 @@ export class TemplateService {
|
||||
title: template.title,
|
||||
type: template.type as any,
|
||||
description: template.description,
|
||||
category: 'Custom', // Add this line
|
||||
category: 'Custom',
|
||||
created_at: template.created_at,
|
||||
lastUpdated: template.updated_at,
|
||||
nostrEventId: template.nostr_event_id || undefined,
|
||||
parentId: template.parent_id || undefined,
|
||||
authorPubkey: template.author_pubkey || undefined,
|
||||
isArchived: template.is_archived === 1,
|
||||
exercises,
|
||||
availability: {
|
||||
source: [template.source as any]
|
||||
@ -79,6 +97,63 @@ export class TemplateService {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all archived templates
|
||||
*/
|
||||
async getArchivedTemplates(limit: number = 50, offset: number = 0): Promise<WorkoutTemplate[]> {
|
||||
try {
|
||||
const templates = await this.db.getAllAsync<{
|
||||
id: string;
|
||||
title: string;
|
||||
type: string;
|
||||
description: string;
|
||||
created_at: number;
|
||||
updated_at: number;
|
||||
nostr_event_id: string | null;
|
||||
source: string;
|
||||
parent_id: string | null;
|
||||
author_pubkey: string | null;
|
||||
is_archived: number;
|
||||
}>(
|
||||
`SELECT * FROM templates WHERE is_archived = 1 ORDER BY updated_at DESC LIMIT ? OFFSET ?`,
|
||||
[limit, offset]
|
||||
);
|
||||
|
||||
const result: WorkoutTemplate[] = [];
|
||||
|
||||
for (const template of templates) {
|
||||
// Get exercises for this template
|
||||
const exercises = await this.getTemplateExercises(template.id);
|
||||
|
||||
result.push({
|
||||
id: template.id,
|
||||
title: template.title,
|
||||
type: template.type as any,
|
||||
description: template.description,
|
||||
category: 'Custom',
|
||||
created_at: template.created_at,
|
||||
lastUpdated: template.updated_at,
|
||||
nostrEventId: template.nostr_event_id || undefined,
|
||||
parentId: template.parent_id || undefined,
|
||||
authorPubkey: template.author_pubkey || undefined,
|
||||
isArchived: true,
|
||||
exercises,
|
||||
availability: {
|
||||
source: [template.source as any]
|
||||
},
|
||||
isPublic: false,
|
||||
version: 1,
|
||||
tags: []
|
||||
});
|
||||
}
|
||||
|
||||
return result;
|
||||
} catch (error) {
|
||||
console.error('Error getting archived templates:', error);
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a template by ID
|
||||
*/
|
||||
@ -94,6 +169,8 @@ export class TemplateService {
|
||||
nostr_event_id: string | null;
|
||||
source: string;
|
||||
parent_id: string | null;
|
||||
author_pubkey: string | null;
|
||||
is_archived: number;
|
||||
}>(
|
||||
`SELECT * FROM templates WHERE id = ?`,
|
||||
[id]
|
||||
@ -114,6 +191,8 @@ export class TemplateService {
|
||||
lastUpdated: template.updated_at,
|
||||
nostrEventId: template.nostr_event_id || undefined,
|
||||
parentId: template.parent_id || undefined,
|
||||
authorPubkey: template.author_pubkey || undefined,
|
||||
isArchived: template.is_archived === 1,
|
||||
exercises,
|
||||
availability: {
|
||||
source: [template.source as any]
|
||||
@ -141,8 +220,8 @@ export class TemplateService {
|
||||
await this.db.runAsync(
|
||||
`INSERT INTO templates (
|
||||
id, title, type, description, created_at, updated_at,
|
||||
nostr_event_id, source, parent_id
|
||||
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
||||
nostr_event_id, source, parent_id, author_pubkey, is_archived
|
||||
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
||||
[
|
||||
id,
|
||||
template.title,
|
||||
@ -152,7 +231,9 @@ export class TemplateService {
|
||||
timestamp,
|
||||
template.nostrEventId || null,
|
||||
template.availability?.source[0] || 'local',
|
||||
template.parentId || null
|
||||
template.parentId || null,
|
||||
template.authorPubkey || null,
|
||||
template.isArchived ? 1 : 0
|
||||
]
|
||||
);
|
||||
|
||||
@ -223,6 +304,15 @@ export class TemplateService {
|
||||
updateValues.push(updates.nostrEventId);
|
||||
}
|
||||
|
||||
if (updates.authorPubkey !== undefined) {
|
||||
updateFields.push('author_pubkey = ?');
|
||||
updateValues.push(updates.authorPubkey);
|
||||
}
|
||||
if (updates.isArchived !== undefined) {
|
||||
updateFields.push('is_archived = ?');
|
||||
updateValues.push(updates.isArchived ? 1 : 0);
|
||||
}
|
||||
|
||||
// Always update the timestamp
|
||||
updateFields.push('updated_at = ?');
|
||||
updateValues.push(timestamp);
|
||||
@ -318,79 +408,127 @@ export class TemplateService {
|
||||
}
|
||||
}
|
||||
|
||||
// Helper methods
|
||||
private async getTemplateExercises(templateId: string): Promise<TemplateExerciseConfig[]> {
|
||||
/**
|
||||
* Archive a template
|
||||
*/
|
||||
async archiveTemplate(id: string, archive: boolean = true): Promise<void> {
|
||||
try {
|
||||
await this.db.runAsync(
|
||||
'UPDATE templates SET is_archived = ? WHERE id = ?',
|
||||
[archive ? 1 : 0, id]
|
||||
);
|
||||
} catch (error) {
|
||||
console.error('Error archiving template:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove template from library
|
||||
*/
|
||||
async removeFromLibrary(id: string): Promise<void> {
|
||||
try {
|
||||
await this.db.withTransactionAsync(async () => {
|
||||
// Delete template-exercise relationships
|
||||
await this.db.runAsync(
|
||||
'DELETE FROM template_exercises WHERE template_id = ?',
|
||||
[id]
|
||||
);
|
||||
|
||||
// Delete template
|
||||
await this.db.runAsync(
|
||||
'DELETE FROM templates WHERE id = ?',
|
||||
[id]
|
||||
);
|
||||
|
||||
// Update powr_pack_items to mark as not imported
|
||||
await this.db.runAsync(
|
||||
'UPDATE powr_pack_items SET is_imported = 0 WHERE item_id = ? AND item_type = "template"',
|
||||
[id]
|
||||
);
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Error removing template from library:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete template from Nostr
|
||||
*/
|
||||
async deleteFromNostr(id: string, ndk: any): Promise<void> {
|
||||
try {
|
||||
// Get template details
|
||||
const template = await this.getTemplate(id);
|
||||
if (!template || !template.nostrEventId) {
|
||||
throw new Error('Template not found or not from Nostr');
|
||||
}
|
||||
|
||||
// Create deletion event
|
||||
const event = new ndk.NDKEvent(ndk);
|
||||
event.kind = 5; // Deletion event
|
||||
event.tags.push(['e', template.nostrEventId]); // Reference to template event
|
||||
event.content = '';
|
||||
|
||||
// Sign and publish
|
||||
await event.sign();
|
||||
await event.publish();
|
||||
|
||||
// Remove from database
|
||||
await this.removeFromLibrary(id);
|
||||
} catch (error) {
|
||||
console.error('Error deleting template from Nostr:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
// Helper methods
|
||||
async getTemplateExercises(templateId: string): Promise<TemplateExerciseWithData[]> {
|
||||
try {
|
||||
// Add additional logging for diagnostic purposes
|
||||
console.log(`Fetching exercises for template ${templateId}`);
|
||||
|
||||
const exercises = await this.db.getAllAsync<{
|
||||
id: string;
|
||||
exercise_id: string;
|
||||
display_order: number;
|
||||
target_sets: number | null;
|
||||
target_reps: number | null;
|
||||
target_weight: number | null;
|
||||
notes: string | null;
|
||||
nostr_reference: string | null;
|
||||
}>(
|
||||
`SELECT
|
||||
te.id, te.exercise_id, te.target_sets, te.target_reps,
|
||||
te.target_weight, te.notes
|
||||
FROM template_exercises te
|
||||
WHERE te.template_id = ?
|
||||
ORDER BY te.display_order`,
|
||||
`SELECT id, exercise_id, display_order, target_sets, target_reps, target_weight, notes, nostr_reference
|
||||
FROM template_exercises
|
||||
WHERE template_id = ?
|
||||
ORDER BY display_order`,
|
||||
[templateId]
|
||||
);
|
||||
|
||||
console.log(`Found ${exercises.length} template exercises in database`);
|
||||
|
||||
// Log exercise IDs for debugging
|
||||
if (exercises.length > 0) {
|
||||
exercises.forEach(ex => console.log(` - Exercise ID: ${ex.exercise_id}`));
|
||||
if (exercises.length === 0) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const result: TemplateExerciseConfig[] = [];
|
||||
// Get the actual exercise data for each template exercise
|
||||
const result: TemplateExerciseWithData[] = [];
|
||||
|
||||
for (const ex of exercises) {
|
||||
// Get exercise details
|
||||
const exercise = await this.db.getFirstAsync<{
|
||||
title: string;
|
||||
type: string;
|
||||
category: string;
|
||||
equipment: string | null;
|
||||
}>(
|
||||
`SELECT title, type, category, equipment FROM exercises WHERE id = ?`,
|
||||
[ex.exercise_id]
|
||||
);
|
||||
|
||||
// Log if exercise is found
|
||||
if (exercise) {
|
||||
console.log(`Found exercise: ${exercise.title} (${ex.exercise_id})`);
|
||||
} else {
|
||||
console.log(`Exercise not found for ID: ${ex.exercise_id}`);
|
||||
|
||||
// Important: Skip exercises that don't exist in the database
|
||||
// We don't want to include placeholder exercises
|
||||
continue;
|
||||
}
|
||||
for (const exerciseRow of exercises) {
|
||||
const exerciseData = await this.exerciseService.getExercise(exerciseRow.exercise_id);
|
||||
|
||||
if (exerciseData) {
|
||||
result.push({
|
||||
id: ex.id,
|
||||
exercise: {
|
||||
id: ex.exercise_id,
|
||||
title: exercise?.title || 'Unknown Exercise',
|
||||
type: exercise?.type as any || 'strength',
|
||||
category: exercise?.category as any || 'Other',
|
||||
equipment: exercise?.equipment as any || undefined,
|
||||
tags: [], // Required property
|
||||
availability: { source: ['local'] }, // Required property
|
||||
created_at: Date.now() // Required property
|
||||
},
|
||||
targetSets: ex.target_sets || undefined,
|
||||
targetReps: ex.target_reps || undefined,
|
||||
targetWeight: ex.target_weight || undefined,
|
||||
notes: ex.notes || undefined
|
||||
id: exerciseRow.id,
|
||||
exercise: exerciseData,
|
||||
displayOrder: exerciseRow.display_order,
|
||||
targetSets: exerciseRow.target_sets ?? undefined, // Convert null to undefined
|
||||
targetReps: exerciseRow.target_reps ?? undefined, // Convert null to undefined
|
||||
targetWeight: exerciseRow.target_weight ?? undefined, // Convert null to undefined
|
||||
notes: exerciseRow.notes ?? undefined, // Convert null to undefined
|
||||
nostrReference: exerciseRow.nostr_reference ?? undefined, // Convert null to undefined
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
console.log(`Returning ${result.length} template exercises`);
|
||||
return result;
|
||||
@ -410,7 +548,7 @@ export class TemplateService {
|
||||
|
||||
// Get database access
|
||||
const db = openDatabaseSync('powr.db');
|
||||
const service = new TemplateService(db);
|
||||
const service = new TemplateService(db, new ExerciseService(db))
|
||||
|
||||
// Get the existing template
|
||||
const template = await service.getTemplate(workout.templateId);
|
||||
@ -455,7 +593,7 @@ export class TemplateService {
|
||||
try {
|
||||
// Get database access
|
||||
const db = openDatabaseSync('powr.db');
|
||||
const service = new TemplateService(db);
|
||||
const service = new TemplateService(db, new ExerciseService(db));
|
||||
|
||||
// Convert workout exercises to template format
|
||||
const exercises: TemplateExerciseConfig[] = workout.exercises.map(ex => ({
|
||||
@ -489,7 +627,8 @@ export class TemplateService {
|
||||
},
|
||||
isPublic: false,
|
||||
version: 1,
|
||||
tags: []
|
||||
tags: [],
|
||||
isArchived: false
|
||||
});
|
||||
|
||||
console.log('New template created from workout:', templateId);
|
||||
|
@ -8,6 +8,7 @@ export function useTemplates() {
|
||||
const [templates, setTemplates] = useState<WorkoutTemplate[]>([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [error, setError] = useState<Error | null>(null);
|
||||
const [archivedTemplates, setArchivedTemplates] = useState<WorkoutTemplate[]>([]);
|
||||
|
||||
const loadTemplates = useCallback(async (limit: number = 50, offset: number = 0) => {
|
||||
try {
|
||||
@ -36,7 +37,15 @@ export function useTemplates() {
|
||||
|
||||
const createTemplate = useCallback(async (template: Omit<WorkoutTemplate, 'id'>) => {
|
||||
try {
|
||||
const id = await templateService.createTemplate(template);
|
||||
// Add default values for new properties
|
||||
const templateWithDefaults = {
|
||||
...template,
|
||||
isArchived: template.isArchived !== undefined ? template.isArchived : false,
|
||||
// Only set authorPubkey if not provided and we have an authenticated user
|
||||
// (you would need to import useNDKCurrentUser from your NDK hooks)
|
||||
};
|
||||
|
||||
const id = await templateService.createTemplate(templateWithDefaults);
|
||||
await loadTemplates(); // Refresh the list
|
||||
return id;
|
||||
} catch (err) {
|
||||
@ -65,6 +74,36 @@ export function useTemplates() {
|
||||
}
|
||||
}, [templateService]);
|
||||
|
||||
// Add new archive/unarchive method
|
||||
const archiveTemplate = useCallback(async (id: string, archive: boolean = true) => {
|
||||
try {
|
||||
await templateService.archiveTemplate(id, archive);
|
||||
await loadTemplates(); // Refresh the list
|
||||
} catch (err) {
|
||||
console.error(`Error ${archive ? 'archiving' : 'unarchiving'} template:`, err);
|
||||
throw err;
|
||||
}
|
||||
}, [templateService, loadTemplates]);
|
||||
|
||||
// Add support for loading archived templates
|
||||
const loadArchivedTemplates = useCallback(async (limit: number = 50, offset: number = 0) => {
|
||||
try {
|
||||
setLoading(true);
|
||||
const data = await templateService.getArchivedTemplates(limit, offset);
|
||||
// You might want to store archived templates in a separate state variable
|
||||
// For now, I'll assume you want to replace the main templates list
|
||||
setTemplates(data);
|
||||
setError(null);
|
||||
setArchivedTemplates(data);
|
||||
} catch (err) {
|
||||
console.error('Error loading archived templates:', err);
|
||||
setError(err instanceof Error ? err : new Error('Failed to load archived templates'));
|
||||
setTemplates([]);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}, [templateService]);
|
||||
|
||||
// Initial load
|
||||
useEffect(() => {
|
||||
loadTemplates();
|
||||
@ -72,13 +111,16 @@ export function useTemplates() {
|
||||
|
||||
return {
|
||||
templates,
|
||||
archivedTemplates,
|
||||
loading,
|
||||
error,
|
||||
loadTemplates,
|
||||
loadArchivedTemplates,
|
||||
getTemplate,
|
||||
createTemplate,
|
||||
updateTemplate,
|
||||
deleteTemplate,
|
||||
archiveTemplate,
|
||||
refreshTemplates: loadTemplates
|
||||
};
|
||||
}
|
8
package-lock.json
generated
8
package-lock.json
generated
@ -91,6 +91,7 @@
|
||||
"@types/lodash": "^4.17.15",
|
||||
"@types/react": "~18.3.12",
|
||||
"@types/react-native": "^0.72.8",
|
||||
"@types/uuid": "^10.0.0",
|
||||
"babel-plugin-module-resolver": "^5.0.2",
|
||||
"expo-haptics": "^14.0.1",
|
||||
"typescript": "^5.3.3"
|
||||
@ -9336,6 +9337,13 @@
|
||||
"integrity": "sha512-/Ad8+nIOV7Rl++6f1BdKxFSMgmoqEoYbHRpPcx3JEfv8VRsQe9Z4mCXeJBzxs7mbHY/XOZZuXlRNfhpVPbs6ZA==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/uuid": {
|
||||
"version": "10.0.0",
|
||||
"resolved": "https://registry.npmjs.org/@types/uuid/-/uuid-10.0.0.tgz",
|
||||
"integrity": "sha512-7gqG38EyHgyP1S+7+xomFtL+ZNHcKv6DwNaCZmJmo1vgMugyF3TCnXVg4t1uk89mLNwnLtnY3TpOpCOyp1/xHQ==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/yargs": {
|
||||
"version": "17.0.33",
|
||||
"resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-17.0.33.tgz",
|
||||
|
@ -105,6 +105,7 @@
|
||||
"@types/lodash": "^4.17.15",
|
||||
"@types/react": "~18.3.12",
|
||||
"@types/react-native": "^0.72.8",
|
||||
"@types/uuid": "^10.0.0",
|
||||
"babel-plugin-module-resolver": "^5.0.2",
|
||||
"expo-haptics": "^14.0.1",
|
||||
"typescript": "^5.3.3"
|
||||
|
@ -27,6 +27,7 @@ import { TemplateService } from '@/lib//db/services/TemplateService';
|
||||
import { WorkoutService } from '@/lib/db/services/WorkoutService'; // Add this import
|
||||
import { NostrEvent } from '@/types/nostr';
|
||||
import { NDKEvent } from '@nostr-dev-kit/ndk-mobile';
|
||||
import { ExerciseService } from '@/lib/db/services/ExerciseService';
|
||||
|
||||
/**
|
||||
* Workout Store
|
||||
@ -817,7 +818,9 @@ async function getTemplate(templateId: string): Promise<WorkoutTemplate | null>
|
||||
// Try to get it from favorites in the database
|
||||
const db = openDatabaseSync('powr.db');
|
||||
const favoritesService = new FavoritesService(db);
|
||||
const templateService = new TemplateService(db);
|
||||
const exerciseService = new ExerciseService(db);
|
||||
const templateService = new TemplateService(db, new ExerciseService(db));
|
||||
|
||||
|
||||
// First try to get from favorites
|
||||
const favoriteResult = await favoritesService.getContentById<WorkoutTemplate>('template', templateId);
|
||||
|
@ -14,6 +14,8 @@ export interface NostrSyncMetadata {
|
||||
pubkey: string;
|
||||
relayUrl: string;
|
||||
created_at: number;
|
||||
dTag?: string;
|
||||
eventId?: string;
|
||||
};
|
||||
}
|
||||
|
||||
|
@ -65,6 +65,17 @@ export interface TemplateExerciseConfig {
|
||||
roundRest?: number;
|
||||
}
|
||||
|
||||
export interface TemplateExerciseWithData {
|
||||
id: string;
|
||||
exercise: BaseExercise;
|
||||
displayOrder: number;
|
||||
targetSets: number | null;
|
||||
targetReps: number | null;
|
||||
targetWeight: number | null;
|
||||
notes: string | null;
|
||||
nostrReference?: string | null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Template versioning and derivation tracking
|
||||
*/
|
||||
@ -124,8 +135,8 @@ export interface WorkoutTemplate extends TemplateBase, SyncableContent {
|
||||
exercises: TemplateExerciseConfig[];
|
||||
isPublic: boolean;
|
||||
version: number;
|
||||
lastUpdated?: number; // Add this line
|
||||
parentId?: string; // Add this line
|
||||
lastUpdated?: number;
|
||||
parentId?: string;
|
||||
|
||||
// Template configuration
|
||||
format?: {
|
||||
@ -151,6 +162,10 @@ export interface WorkoutTemplate extends TemplateBase, SyncableContent {
|
||||
// Nostr integration
|
||||
nostrEventId?: string;
|
||||
relayUrls?: string[];
|
||||
authorPubkey?: string;
|
||||
|
||||
// Template management
|
||||
isArchived?: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
|
46
utils/ids.ts
46
utils/ids.ts
@ -1,4 +1,5 @@
|
||||
// utils/ids.ts
|
||||
import { v4 as uuidv4 } from 'uuid'; // You'll need to add this dependency
|
||||
|
||||
/**
|
||||
* Generates a unique identifier with optional source prefix
|
||||
@ -6,25 +7,45 @@
|
||||
* @returns A unique string identifier
|
||||
*/
|
||||
export function generateId(source: 'local' | 'nostr' = 'local'): string {
|
||||
// For local IDs, use the current format with a prefix
|
||||
if (source === 'local') {
|
||||
// Generate timestamp and random parts
|
||||
const timestamp = Date.now().toString(36);
|
||||
const randomPart = Math.random().toString(36).substring(2, 15);
|
||||
|
||||
// For local IDs, use the current format with a prefix
|
||||
if (source === 'local') {
|
||||
return `local:${timestamp}-${randomPart}`;
|
||||
}
|
||||
|
||||
// For Nostr-compatible IDs (temporary until we integrate actual Nostr)
|
||||
// This creates a similar format to Nostr but is clearly marked as temporary
|
||||
return `nostr:temp:${timestamp}-${randomPart}`;
|
||||
// For Nostr IDs, use proper UUID format
|
||||
return uuidv4();
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates a Nostr-compatible d-tag for addressable events
|
||||
* @param type - Optional type identifier for the d-tag, e.g., 'exercise', 'template'
|
||||
* @param ensureUnique - Optional boolean to ensure the d-tag is always globally unique
|
||||
* @returns A string to use as the d-tag value
|
||||
*/
|
||||
export function generateDTag(type: string = '', ensureUnique: boolean = true): string {
|
||||
if (ensureUnique) {
|
||||
// If we need global uniqueness, generate a short UUID-based tag
|
||||
const shortId = uuidv4().substring(0, 12);
|
||||
return type ? `${type}-${shortId}` : shortId;
|
||||
} else {
|
||||
// For local uniqueness (e.g., per-user), a simpler ID may suffice
|
||||
const timestamp = Date.now().toString(36);
|
||||
const randomPart = Math.random().toString(36).substring(2, 8);
|
||||
return type ? `${type}-${timestamp}${randomPart}` : `${timestamp}${randomPart}`;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if an ID is a Nostr event ID or temporary Nostr-format ID
|
||||
*/
|
||||
export function isNostrId(id: string): boolean {
|
||||
return id.startsWith('note1') || id.startsWith('nostr:');
|
||||
// Check for standard Nostr bech32 encoding or our temporary format
|
||||
return id.startsWith('note1') || id.startsWith('nostr:') ||
|
||||
// Also check for UUID format (for new Nostr event IDs)
|
||||
/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(id);
|
||||
}
|
||||
|
||||
/**
|
||||
@ -46,3 +67,14 @@ export function generateId(source: 'local' | 'nostr' = 'local'): string {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a Nostr addressable reference (NIP-01/33)
|
||||
* @param kind - The Nostr event kind
|
||||
* @param pubkey - The author's public key
|
||||
* @param dTag - The d-tag value for the addressable event
|
||||
* @returns A string in the format "kind:pubkey:d-tag"
|
||||
*/
|
||||
export function createNostrReference(kind: number, pubkey: string, dTag: string): string {
|
||||
return `${kind}:${pubkey}:${dTag}`;
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user