2025-03-13 14:02:36 -04:00
|
|
|
// lib/db/services/POWRPackService.ts
|
|
|
|
import { SQLiteDatabase } from 'expo-sqlite';
|
|
|
|
import NDK, { NDKEvent, NDKFilter } from '@nostr-dev-kit/ndk-mobile';
|
|
|
|
import { nip19 } from 'nostr-tools';
|
2025-03-13 22:39:28 -04:00
|
|
|
import { generateId } from '@/utils/ids';
|
2025-03-13 14:02:36 -04:00
|
|
|
import { NostrIntegration } from './NostrIntegration';
|
2025-03-13 22:39:28 -04:00
|
|
|
import { POWRPack, POWRPackImport, POWRPackSelection, POWRPackWithContent } from '@/types/powr-pack';
|
|
|
|
import {
|
|
|
|
BaseExercise,
|
|
|
|
ExerciseType,
|
|
|
|
ExerciseCategory
|
|
|
|
} from '@/types/exercise';
|
|
|
|
import {
|
|
|
|
WorkoutTemplate,
|
|
|
|
TemplateType
|
|
|
|
} from '@/types/templates';
|
2025-03-13 14:02:36 -04:00
|
|
|
|
2025-03-13 22:39:28 -04:00
|
|
|
/**
|
|
|
|
* Service for managing POWR Packs (importable collections of templates and exercises)
|
|
|
|
*/
|
|
|
|
export default class POWRPackService {
|
2025-03-13 14:02:36 -04:00
|
|
|
private db: SQLiteDatabase;
|
2025-03-13 22:39:28 -04:00
|
|
|
private nostrIntegration: NostrIntegration;
|
2025-03-13 14:02:36 -04:00
|
|
|
|
|
|
|
constructor(db: SQLiteDatabase) {
|
|
|
|
this.db = db;
|
2025-03-13 22:39:28 -04:00
|
|
|
this.nostrIntegration = new NostrIntegration(db);
|
2025-03-13 14:02:36 -04:00
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
2025-03-13 22:39:28 -04:00
|
|
|
* Fetch a POWR Pack from a Nostr address (naddr)
|
2025-03-13 14:02:36 -04:00
|
|
|
*/
|
|
|
|
async fetchPackFromNaddr(naddr: string, ndk: NDK): Promise<POWRPackImport> {
|
|
|
|
try {
|
|
|
|
console.log(`Fetching POWR Pack from naddr: ${naddr}`);
|
|
|
|
|
2025-03-13 22:39:28 -04:00
|
|
|
// Validate naddr format
|
|
|
|
if (!naddr.startsWith('naddr1')) {
|
|
|
|
throw new Error('Invalid naddr format. Should start with "naddr1"');
|
|
|
|
}
|
|
|
|
|
|
|
|
// Decode naddr
|
2025-03-13 14:02:36 -04:00
|
|
|
const decoded = nip19.decode(naddr);
|
|
|
|
if (decoded.type !== 'naddr') {
|
|
|
|
throw new Error('Invalid naddr format');
|
|
|
|
}
|
|
|
|
|
2025-03-13 22:39:28 -04:00
|
|
|
const { pubkey, kind, identifier } = decoded.data;
|
2025-03-13 14:02:36 -04:00
|
|
|
console.log(`Decoded naddr: pubkey=${pubkey}, kind=${kind}, identifier=${identifier}`);
|
|
|
|
|
2025-03-13 22:39:28 -04:00
|
|
|
// Create filter to fetch the pack event
|
2025-03-13 14:02:36 -04:00
|
|
|
const packFilter: NDKFilter = {
|
|
|
|
kinds: [kind],
|
|
|
|
authors: [pubkey],
|
|
|
|
'#d': identifier ? [identifier] : undefined
|
|
|
|
};
|
|
|
|
|
|
|
|
console.log(`Fetching pack with filter: ${JSON.stringify(packFilter)}`);
|
|
|
|
|
2025-03-13 22:39:28 -04:00
|
|
|
// Fetch the pack event
|
|
|
|
const events = await ndk.fetchEvents(packFilter);
|
|
|
|
if (events.size === 0) {
|
2025-03-13 14:02:36 -04:00
|
|
|
throw new Error('Pack not found');
|
|
|
|
}
|
|
|
|
|
2025-03-13 22:39:28 -04:00
|
|
|
// Get the first matching event
|
|
|
|
const packEvent = Array.from(events)[0];
|
2025-03-13 14:02:36 -04:00
|
|
|
console.log(`Fetched pack event: ${packEvent.id}`);
|
2025-03-13 22:39:28 -04:00
|
|
|
|
|
|
|
// Get tags for debugging
|
2025-03-13 14:02:36 -04:00
|
|
|
console.log(`Pack tags: ${JSON.stringify(packEvent.tags)}`);
|
|
|
|
|
2025-03-13 22:39:28 -04:00
|
|
|
// Extract template and exercise references
|
2025-03-13 14:02:36 -04:00
|
|
|
const templateRefs: string[] = [];
|
|
|
|
const exerciseRefs: string[] = [];
|
|
|
|
|
2025-03-13 22:39:28 -04:00
|
|
|
// Use NDK's getMatchingTags for more reliable tag handling
|
|
|
|
const aTags = packEvent.getMatchingTags('a');
|
|
|
|
|
|
|
|
for (const tag of aTags) {
|
|
|
|
if (tag.length < 2) continue;
|
|
|
|
|
|
|
|
const addressPointer = tag[1];
|
|
|
|
if (addressPointer.startsWith('33402:')) {
|
|
|
|
console.log(`Found template reference: ${addressPointer}`);
|
|
|
|
templateRefs.push(addressPointer);
|
|
|
|
} else if (addressPointer.startsWith('33401:')) {
|
|
|
|
console.log(`Found exercise reference: ${addressPointer}`);
|
|
|
|
exerciseRefs.push(addressPointer);
|
2025-03-13 14:02:36 -04:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
console.log(`Found ${templateRefs.length} template refs and ${exerciseRefs.length} exercise refs`);
|
|
|
|
|
2025-03-13 22:39:28 -04:00
|
|
|
// Fetch referenced templates and exercises
|
2025-03-13 14:02:36 -04:00
|
|
|
const templates = await this.fetchReferencedEvents(ndk, templateRefs);
|
|
|
|
const exercises = await this.fetchReferencedEvents(ndk, exerciseRefs);
|
|
|
|
|
|
|
|
console.log(`Fetched ${templates.length} templates and ${exercises.length} exercises`);
|
|
|
|
|
|
|
|
return {
|
|
|
|
packEvent,
|
|
|
|
templates,
|
|
|
|
exercises
|
|
|
|
};
|
|
|
|
} catch (error) {
|
|
|
|
console.error('Error fetching pack from naddr:', error);
|
|
|
|
throw error;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
2025-03-13 22:39:28 -04:00
|
|
|
* Fetch referenced events (templates or exercises)
|
2025-03-13 14:02:36 -04:00
|
|
|
*/
|
2025-03-13 22:39:28 -04:00
|
|
|
async fetchReferencedEvents(ndk: NDK, refs: string[]): Promise<NDKEvent[]> {
|
|
|
|
if (refs.length === 0) return [];
|
2025-03-13 14:02:36 -04:00
|
|
|
|
2025-03-13 22:39:28 -04:00
|
|
|
console.log(`Fetching references: ${JSON.stringify(refs)}`);
|
2025-03-13 14:02:36 -04:00
|
|
|
|
2025-03-13 22:39:28 -04:00
|
|
|
const events: NDKEvent[] = [];
|
|
|
|
|
|
|
|
for (const ref of refs) {
|
2025-03-13 14:02:36 -04:00
|
|
|
try {
|
2025-03-13 22:39:28 -04:00
|
|
|
// Parse the reference format (kind:pubkey:d-tag)
|
|
|
|
const [kindStr, pubkey, dTag] = ref.split(':');
|
2025-03-13 14:02:36 -04:00
|
|
|
const kind = parseInt(kindStr);
|
|
|
|
|
2025-03-13 22:39:28 -04:00
|
|
|
console.log(`Fetching ${kind} event with d-tag ${dTag} from author ${pubkey}`);
|
2025-03-13 14:02:36 -04:00
|
|
|
|
2025-03-13 22:39:28 -04:00
|
|
|
// Create a filter to find this specific event
|
2025-03-13 14:02:36 -04:00
|
|
|
const filter: NDKFilter = {
|
|
|
|
kinds: [kind],
|
2025-03-13 22:39:28 -04:00
|
|
|
authors: [pubkey],
|
|
|
|
'#d': [dTag]
|
2025-03-13 14:02:36 -04:00
|
|
|
};
|
|
|
|
|
2025-03-13 22:39:28 -04:00
|
|
|
// Try to fetch by filter first
|
|
|
|
const fetchedEvents = await ndk.fetchEvents(filter);
|
2025-03-13 14:02:36 -04:00
|
|
|
|
|
|
|
if (fetchedEvents.size > 0) {
|
2025-03-13 22:39:28 -04:00
|
|
|
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}`);
|
2025-03-13 14:02:36 -04:00
|
|
|
}
|
|
|
|
} catch (error) {
|
2025-03-13 22:39:28 -04:00
|
|
|
console.error(`Error fetching reference ${ref}:`, error);
|
2025-03-13 14:02:36 -04:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
console.log(`Total fetched referenced events: ${events.length}`);
|
|
|
|
return events;
|
|
|
|
}
|
|
|
|
|
2025-03-13 22:39:28 -04:00
|
|
|
/**
|
|
|
|
* Analyze dependencies between templates and exercises
|
|
|
|
*/
|
|
|
|
analyzeDependencies(templates: NDKEvent[], exercises: NDKEvent[]): Record<string, string[]> {
|
|
|
|
const dependencies: Record<string, string[]> = {};
|
|
|
|
const exerciseMap = new Map<string, NDKEvent>();
|
|
|
|
|
|
|
|
// Map exercises by "kind:pubkey:d-tag" for easier lookup
|
|
|
|
for (const exercise of exercises) {
|
|
|
|
const dTag = exercise.tagValue('d');
|
|
|
|
if (dTag) {
|
|
|
|
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';
|
2025-03-13 14:02:36 -04:00
|
|
|
|
2025-03-13 22:39:28 -04:00
|
|
|
console.log(`Analyzing template ${templateName} (${templateId})`);
|
|
|
|
dependencies[templateId] = [];
|
2025-03-13 14:02:36 -04:00
|
|
|
|
2025-03-13 22:39:28 -04:00
|
|
|
// Get exercise references from template
|
|
|
|
const exerciseTags = template.getMatchingTags('exercise');
|
2025-03-13 14:02:36 -04:00
|
|
|
|
2025-03-13 22:39:28 -04:00
|
|
|
for (const tag of exerciseTags) {
|
|
|
|
if (tag.length < 2) continue;
|
2025-03-13 14:02:36 -04:00
|
|
|
|
2025-03-13 22:39:28 -04:00
|
|
|
const exerciseRef = tag[1];
|
|
|
|
console.log(`Template ${templateName} references ${exerciseRef}`);
|
2025-03-13 14:02:36 -04:00
|
|
|
|
2025-03-13 22:39:28 -04:00
|
|
|
// 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(`Template ${templateName} references unknown exercise ${exerciseRef}`);
|
|
|
|
}
|
|
|
|
}
|
2025-03-13 14:02:36 -04:00
|
|
|
|
2025-03-13 22:39:28 -04:00
|
|
|
console.log(`Template ${templateName} has ${dependencies[templateId].length} dependencies`);
|
|
|
|
}
|
2025-03-13 14:02:36 -04:00
|
|
|
|
2025-03-13 22:39:28 -04:00
|
|
|
return dependencies;
|
|
|
|
}
|
|
|
|
|
2025-03-13 14:02:36 -04:00
|
|
|
/**
|
2025-03-13 22:39:28 -04:00
|
|
|
* Import a POWR Pack into the local database
|
2025-03-13 14:02:36 -04:00
|
|
|
*/
|
2025-03-13 22:39:28 -04:00
|
|
|
async importPack(packImport: POWRPackImport, selection: POWRPackSelection): Promise<void> {
|
2025-03-13 14:02:36 -04:00
|
|
|
try {
|
2025-03-13 22:39:28 -04:00
|
|
|
console.log(`Importing ${selection.selectedExercises.length} exercises...`);
|
2025-03-13 14:02:36 -04:00
|
|
|
|
2025-03-13 22:39:28 -04:00
|
|
|
// Map to track imported exercise IDs by various reference formats
|
|
|
|
const exerciseIdMap = new Map<string, string>();
|
2025-03-13 14:02:36 -04:00
|
|
|
|
2025-03-13 22:39:28 -04:00
|
|
|
// First, import the selected exercises
|
|
|
|
for (const exerciseId of selection.selectedExercises) {
|
|
|
|
const exerciseEvent = packImport.exercises.find(e => e.id === exerciseId);
|
|
|
|
if (!exerciseEvent) continue;
|
|
|
|
|
|
|
|
// Get the d-tag value from the event
|
|
|
|
const dTag = exerciseEvent.tagValue('d');
|
|
|
|
|
|
|
|
// Convert to local model
|
|
|
|
const exerciseModel = this.nostrIntegration.convertNostrExerciseToLocal(exerciseEvent);
|
|
|
|
|
|
|
|
// Save to database
|
|
|
|
const localId = await this.nostrIntegration.saveImportedExercise(exerciseModel, exerciseEvent);
|
|
|
|
|
|
|
|
// Map ALL possible ways to reference this exercise:
|
|
|
|
|
|
|
|
// 1. By event ID directly (fallback)
|
|
|
|
exerciseIdMap.set(exerciseId, localId);
|
|
|
|
|
|
|
|
// 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}`);
|
|
|
|
}
|
|
|
|
|
|
|
|
// 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(`Imported exercise: ${exerciseModel.title} (${localId}) from Nostr event ${exerciseId}`);
|
|
|
|
}
|
2025-03-13 14:02:36 -04:00
|
|
|
|
2025-03-13 22:39:28 -04:00
|
|
|
console.log(`Importing ${selection.selectedTemplates.length} templates...`);
|
2025-03-13 14:02:36 -04:00
|
|
|
|
2025-03-13 22:39:28 -04:00
|
|
|
// Then, import the selected templates
|
|
|
|
for (const templateId of selection.selectedTemplates) {
|
|
|
|
const templateEvent = packImport.templates.find(t => t.id === templateId);
|
|
|
|
if (!templateEvent) continue;
|
2025-03-13 14:02:36 -04:00
|
|
|
|
2025-03-13 22:39:28 -04:00
|
|
|
// Convert to local model
|
|
|
|
const templateModel = this.nostrIntegration.convertNostrTemplateToLocal(templateEvent);
|
2025-03-13 14:02:36 -04:00
|
|
|
|
2025-03-13 22:39:28 -04:00
|
|
|
// Save to database
|
|
|
|
const localTemplateId = await this.nostrIntegration.saveImportedTemplate(templateModel, templateEvent);
|
2025-03-13 14:02:36 -04:00
|
|
|
|
2025-03-13 22:39:28 -04:00
|
|
|
console.log(`Imported template: ${templateModel.title} (${localTemplateId}) from Nostr event ${templateId}`);
|
2025-03-13 14:02:36 -04:00
|
|
|
|
2025-03-13 22:39:28 -04:00
|
|
|
// Get exercise references from this template
|
|
|
|
const exerciseRefs = this.nostrIntegration.getTemplateExerciseRefs(templateEvent);
|
2025-03-13 14:02:36 -04:00
|
|
|
|
2025-03-13 22:39:28 -04:00
|
|
|
console.log(`Template has ${exerciseRefs.length} exercise references:`);
|
|
|
|
exerciseRefs.forEach(ref => console.log(` - ${ref}`));
|
2025-03-13 14:02:36 -04:00
|
|
|
|
2025-03-13 22:39:28 -04:00
|
|
|
// Map exercise references to local exercise IDs
|
|
|
|
const templateExerciseIds: string[] = [];
|
|
|
|
const matchedRefs: string[] = [];
|
|
|
|
|
|
|
|
for (const ref of exerciseRefs) {
|
|
|
|
// Extract the base reference (before any parameters)
|
|
|
|
const refParts = ref.split('::');
|
|
|
|
const baseRef = refParts[0];
|
2025-03-13 14:02:36 -04:00
|
|
|
|
2025-03-13 22:39:28 -04:00
|
|
|
console.log(`Looking for matching exercise for reference: ${baseRef}`);
|
2025-03-13 14:02:36 -04:00
|
|
|
|
2025-03-13 22:39:28 -04:00
|
|
|
// Check if we have this reference in our map
|
|
|
|
if (exerciseIdMap.has(baseRef)) {
|
|
|
|
const localExerciseId = exerciseIdMap.get(baseRef) || '';
|
|
|
|
templateExerciseIds.push(localExerciseId);
|
|
|
|
matchedRefs.push(ref);
|
|
|
|
|
|
|
|
console.log(`Mapped reference ${baseRef} to local exercise ID ${localExerciseId}`);
|
|
|
|
continue;
|
|
|
|
}
|
2025-03-13 14:02:36 -04:00
|
|
|
|
2025-03-13 22:39:28 -04:00
|
|
|
// 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...`);
|
2025-03-13 14:02:36 -04:00
|
|
|
|
2025-03-13 22:39:28 -04:00
|
|
|
// 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];
|
2025-03-13 14:02:36 -04:00
|
|
|
|
2025-03-13 22:39:28 -04:00
|
|
|
// 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;
|
2025-03-13 14:02:36 -04:00
|
|
|
}
|
2025-03-13 22:39:28 -04:00
|
|
|
}
|
2025-03-13 14:02:36 -04:00
|
|
|
|
2025-03-13 22:39:28 -04:00
|
|
|
// 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);
|
2025-03-13 14:02:36 -04:00
|
|
|
|
2025-03-13 22:39:28 -04:00
|
|
|
if (matchingEvent && exerciseIdMap.has(matchingEvent.id)) {
|
|
|
|
const localExerciseId = exerciseIdMap.get(matchingEvent.id) || '';
|
|
|
|
templateExerciseIds.push(localExerciseId);
|
|
|
|
matchedRefs.push(ref);
|
|
|
|
|
|
|
|
// 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 components: kind=${refKind}, pubkey=${refPubkey}, d-tag=${refDTag}`);
|
|
|
|
}
|
2025-03-13 14:02:36 -04:00
|
|
|
}
|
|
|
|
} else {
|
2025-03-13 22:39:28 -04:00
|
|
|
console.log(`Invalid reference format: ${baseRef}`);
|
2025-03-13 14:02:36 -04:00
|
|
|
}
|
2025-03-13 22:39:28 -04:00
|
|
|
}
|
|
|
|
|
|
|
|
// Save template-exercise relationships with parameters
|
|
|
|
if (templateExerciseIds.length > 0) {
|
|
|
|
await this.nostrIntegration.saveTemplateExercisesWithParams(
|
|
|
|
localTemplateId,
|
|
|
|
templateExerciseIds,
|
|
|
|
matchedRefs
|
|
|
|
);
|
2025-03-13 14:02:36 -04:00
|
|
|
|
2025-03-13 22:39:28 -04:00
|
|
|
// 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]
|
2025-03-13 14:02:36 -04:00
|
|
|
);
|
2025-03-13 22:39:28 -04:00
|
|
|
console.log(`Template ${templateModel.title} has ${templateExercises.length} exercises associated`);
|
|
|
|
} else {
|
|
|
|
console.log(`No exercise relationships to save for template ${localTemplateId}`);
|
2025-03-13 14:02:36 -04:00
|
|
|
}
|
2025-03-13 22:39:28 -04:00
|
|
|
}
|
|
|
|
|
|
|
|
// Finally, save the pack itself
|
|
|
|
await this.savePack(packImport.packEvent, selection);
|
|
|
|
|
|
|
|
// Get total counts
|
|
|
|
const totalNostrTemplates = await this.db.getFirstAsync<{ count: number }>(
|
|
|
|
`SELECT COUNT(*) as count FROM templates WHERE source = 'nostr'`
|
|
|
|
);
|
|
|
|
|
|
|
|
console.log(`Total nostr templates in database: ${totalNostrTemplates?.count || 0}`);
|
|
|
|
|
|
|
|
// 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:`);
|
|
|
|
templates.forEach(t => {
|
|
|
|
console.log(` - ${t.title}: ${t.id}`);
|
2025-03-13 14:02:36 -04:00
|
|
|
});
|
|
|
|
} catch (error) {
|
2025-03-13 22:39:28 -04:00
|
|
|
console.error('Error importing pack:', error);
|
2025-03-13 14:02:36 -04:00
|
|
|
throw error;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
2025-03-13 22:39:28 -04:00
|
|
|
* Save the pack metadata to the database
|
2025-03-13 14:02:36 -04:00
|
|
|
*/
|
2025-03-13 22:39:28 -04:00
|
|
|
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 (?, ?, ?, ?, ?, ?)`,
|
|
|
|
[
|
|
|
|
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;
|
|
|
|
}
|
2025-03-13 14:02:36 -04:00
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
2025-03-13 22:39:28 -04:00
|
|
|
* Get all imported packs
|
|
|
|
*/
|
2025-03-13 14:02:36 -04:00
|
|
|
async getImportedPacks(): Promise<POWRPackWithContent[]> {
|
|
|
|
try {
|
2025-03-13 22:39:28 -04:00
|
|
|
// 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`
|
2025-03-13 14:02:36 -04:00
|
|
|
);
|
|
|
|
|
2025-03-13 22:39:28 -04:00
|
|
|
// For each pack, get its templates and exercises
|
2025-03-13 14:02:36 -04:00
|
|
|
const result: POWRPackWithContent[] = [];
|
|
|
|
|
2025-03-13 22:39:28 -04:00
|
|
|
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`,
|
2025-03-13 14:02:36 -04:00
|
|
|
[pack.id]
|
|
|
|
);
|
|
|
|
|
2025-03-13 22:39:28 -04:00
|
|
|
// 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`,
|
2025-03-13 14:02:36 -04:00
|
|
|
[pack.id]
|
|
|
|
);
|
|
|
|
|
2025-03-13 22:39:28 -04:00
|
|
|
// 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']
|
|
|
|
}
|
|
|
|
}));
|
|
|
|
|
2025-03-13 14:02:36 -04:00
|
|
|
result.push({
|
|
|
|
pack,
|
2025-03-13 22:39:28 -04:00
|
|
|
templates,
|
|
|
|
exercises
|
2025-03-13 14:02:36 -04:00
|
|
|
});
|
|
|
|
}
|
|
|
|
|
|
|
|
return result;
|
|
|
|
} catch (error) {
|
|
|
|
console.error('Error getting imported packs:', error);
|
2025-03-13 22:39:28 -04:00
|
|
|
return [];
|
2025-03-13 14:02:36 -04:00
|
|
|
}
|
|
|
|
}
|
2025-03-13 22:39:28 -04:00
|
|
|
|
|
|
|
/**
|
|
|
|
* Create a shareable naddr for a POWR Pack
|
|
|
|
* @param packEvent The Nostr event for the pack
|
|
|
|
* @returns A shareable naddr string
|
2025-03-13 14:02:36 -04:00
|
|
|
*/
|
2025-03-13 22:39:28 -04:00
|
|
|
createShareableNaddr(packEvent: NDKEvent): string {
|
2025-03-13 14:02:36 -04:00
|
|
|
try {
|
2025-03-13 22:39:28 -04:00
|
|
|
// Extract d-tag for the pack (required for naddr)
|
|
|
|
const dTag = packEvent.tagValue('d');
|
2025-03-13 14:02:36 -04:00
|
|
|
|
2025-03-13 22:39:28 -04:00
|
|
|
if (!dTag) {
|
|
|
|
throw new Error('Pack event missing required d-tag');
|
2025-03-13 14:02:36 -04:00
|
|
|
}
|
|
|
|
|
2025-03-13 22:39:28 -04:00
|
|
|
// Create naddr using NDK's methods
|
|
|
|
const naddr = packEvent.encode();
|
|
|
|
return naddr;
|
2025-03-13 14:02:36 -04:00
|
|
|
} catch (error) {
|
2025-03-13 22:39:28 -04:00
|
|
|
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');
|
|
|
|
}
|
2025-03-13 14:02:36 -04:00
|
|
|
}
|
|
|
|
}
|
2025-03-13 22:39:28 -04:00
|
|
|
|
2025-03-13 14:02:36 -04:00
|
|
|
/**
|
2025-03-13 22:39:28 -04:00
|
|
|
* Delete a POWR Pack
|
|
|
|
* @param packId The ID of the pack to delete
|
|
|
|
* @param keepItems Whether to keep the imported templates and exercises
|
2025-03-13 14:02:36 -04:00
|
|
|
*/
|
2025-03-13 22:39:28 -04:00
|
|
|
async deletePack(packId: string, keepItems: boolean = true): Promise<void> {
|
2025-03-13 14:02:36 -04:00
|
|
|
try {
|
2025-03-13 22:39:28 -04:00
|
|
|
if (!keepItems) {
|
|
|
|
// 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'`,
|
2025-03-13 14:02:36 -04:00
|
|
|
[packId]
|
|
|
|
);
|
|
|
|
|
2025-03-13 22:39:28 -04:00
|
|
|
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'`,
|
2025-03-13 14:02:36 -04:00
|
|
|
[packId]
|
|
|
|
);
|
2025-03-13 22:39:28 -04:00
|
|
|
|
|
|
|
// 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]
|
|
|
|
);
|
|
|
|
}
|
|
|
|
}
|
2025-03-13 14:02:36 -04:00
|
|
|
|
2025-03-13 22:39:28 -04:00
|
|
|
// Delete the pack items
|
|
|
|
await this.db.runAsync(
|
|
|
|
`DELETE FROM powr_pack_items WHERE pack_id = ?`,
|
|
|
|
[packId]
|
|
|
|
);
|
|
|
|
|
|
|
|
// Finally, delete the pack itself
|
|
|
|
await this.db.runAsync(
|
|
|
|
`DELETE FROM powr_packs WHERE id = ?`,
|
|
|
|
[packId]
|
|
|
|
);
|
2025-03-13 14:02:36 -04:00
|
|
|
} catch (error) {
|
2025-03-13 22:39:28 -04:00
|
|
|
console.error('Error deleting pack:', error);
|
2025-03-13 14:02:36 -04:00
|
|
|
throw error;
|
|
|
|
}
|
|
|
|
}
|
2025-03-13 22:39:28 -04:00
|
|
|
}
|