mirror of
https://github.com/DocNR/POWR.git
synced 2025-05-19 16:32:07 +00:00
657 lines
22 KiB
TypeScript
657 lines
22 KiB
TypeScript
// lib/db/services/NostrIntegration.ts
|
|
import { SQLiteDatabase } from 'expo-sqlite';
|
|
import { NDKEvent } from '@nostr-dev-kit/ndk-mobile';
|
|
import {
|
|
BaseExercise,
|
|
ExerciseType,
|
|
ExerciseCategory,
|
|
Equipment,
|
|
ExerciseFormat,
|
|
ExerciseFormatUnits
|
|
} from '@/types/exercise';
|
|
import {
|
|
WorkoutTemplate,
|
|
TemplateType,
|
|
TemplateCategory,
|
|
TemplateExerciseConfig
|
|
} from '@/types/templates';
|
|
import { generateId } from '@/utils/ids';
|
|
|
|
/**
|
|
* Helper class for converting between Nostr events and local models
|
|
*/
|
|
export class NostrIntegration {
|
|
private db: SQLiteDatabase;
|
|
|
|
constructor(db: SQLiteDatabase) {
|
|
this.db = db;
|
|
}
|
|
|
|
/**
|
|
* Convert a Nostr exercise event to a local Exercise model
|
|
*/
|
|
convertNostrExerciseToLocal(exerciseEvent: NDKEvent): BaseExercise {
|
|
const id = generateId();
|
|
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 = exerciseEvent.getMatchingTags('t').map(tag => tag[1]);
|
|
|
|
// Map equipment to valid type
|
|
const equipment: Equipment = this.mapToValidEquipment(equipmentTag);
|
|
|
|
// Map to valid exercise type
|
|
const type: ExerciseType = this.mapEquipmentToType(equipment);
|
|
|
|
// Map to valid category (using first tag if available)
|
|
const category: ExerciseCategory = this.mapToCategory(tags[0] || '');
|
|
|
|
// Parse format and format_units
|
|
const format: ExerciseFormat = {};
|
|
const formatUnits: ExerciseFormatUnits = {};
|
|
|
|
if (formatTag.length > 0 && formatUnitsTag.length > 0 && formatTag[0].length > 1 && formatUnitsTag[0].length > 1) {
|
|
// Process format parameters
|
|
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;
|
|
formatUnits.weight = (unit === 'kg' || unit === 'lbs') ? unit : 'kg';
|
|
} else if (param === 'reps') {
|
|
format.reps = true;
|
|
formatUnits.reps = 'count';
|
|
} else if (param === 'rpe') {
|
|
format.rpe = true;
|
|
formatUnits.rpe = '0-10';
|
|
} else if (param === 'set_type') {
|
|
format.set_type = true;
|
|
formatUnits.set_type = 'warmup|normal|drop|failure';
|
|
}
|
|
}
|
|
} else {
|
|
// Set default format if none provided
|
|
format.weight = true;
|
|
format.reps = true;
|
|
format.rpe = true;
|
|
format.set_type = true;
|
|
|
|
formatUnits.weight = 'kg';
|
|
formatUnits.reps = 'count';
|
|
formatUnits.rpe = '0-10';
|
|
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,
|
|
title,
|
|
type,
|
|
category,
|
|
equipment,
|
|
description: exerciseEvent.content,
|
|
tags,
|
|
format,
|
|
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
|
|
*/
|
|
private mapToValidEquipment(equipment: string): Equipment {
|
|
switch (equipment.toLowerCase()) {
|
|
case 'barbell':
|
|
return 'barbell';
|
|
case 'dumbbell':
|
|
return 'dumbbell';
|
|
case 'kettlebell':
|
|
return 'kettlebell';
|
|
case 'machine':
|
|
return 'machine';
|
|
case 'cable':
|
|
return 'cable';
|
|
case 'bodyweight':
|
|
return 'bodyweight';
|
|
default:
|
|
return 'other';
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Map Equipment value to exercise type
|
|
*/
|
|
private mapEquipmentToType(equipment: Equipment): ExerciseType {
|
|
switch (equipment) {
|
|
case 'barbell':
|
|
case 'dumbbell':
|
|
case 'kettlebell':
|
|
case 'machine':
|
|
case 'cable':
|
|
case 'other':
|
|
return 'strength';
|
|
case 'bodyweight':
|
|
return 'bodyweight';
|
|
default:
|
|
return 'strength';
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Map string to valid category
|
|
*/
|
|
private mapToCategory(category: string): ExerciseCategory {
|
|
const normalized = category.toLowerCase();
|
|
|
|
if (normalized.includes('push')) return 'Push';
|
|
if (normalized.includes('pull')) return 'Pull';
|
|
if (normalized.includes('leg')) return 'Legs';
|
|
if (normalized.includes('core') || normalized.includes('abs')) return 'Core';
|
|
|
|
// Default to Push if no match
|
|
return 'Push';
|
|
}
|
|
|
|
/**
|
|
* Convert a Nostr template event to a local Template model
|
|
*/
|
|
convertNostrTemplateToLocal(templateEvent: NDKEvent): WorkoutTemplate {
|
|
const id = generateId();
|
|
const title = templateEvent.tagValue('title') || 'Unnamed Template';
|
|
const typeTag = templateEvent.tagValue('type') || 'strength';
|
|
|
|
// Convert string to valid TemplateType
|
|
const type: TemplateType =
|
|
(typeTag === 'strength' || typeTag === 'circuit' ||
|
|
typeTag === 'emom' || typeTag === 'amrap') ?
|
|
typeTag as TemplateType : 'strength';
|
|
|
|
// Get rounds, duration, interval if available
|
|
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 = templateEvent.getMatchingTags('t').map(tag => tag[1]);
|
|
|
|
// Map to valid category
|
|
const category: TemplateCategory = this.mapToTemplateCategory(tags[0] || '');
|
|
|
|
// 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,
|
|
title,
|
|
type,
|
|
category,
|
|
description: templateEvent.content,
|
|
tags,
|
|
rounds,
|
|
duration,
|
|
interval,
|
|
exercises,
|
|
isPublic: true,
|
|
version: 1,
|
|
availability: {
|
|
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,
|
|
authorPubkey: templateEvent.pubkey
|
|
};
|
|
|
|
return template;
|
|
}
|
|
|
|
/**
|
|
* Map string to valid template category
|
|
*/
|
|
private mapToTemplateCategory(category: string): TemplateCategory {
|
|
const normalized = category.toLowerCase();
|
|
|
|
if (normalized.includes('full') && normalized.includes('body')) return 'Full Body';
|
|
if (normalized.includes('push') || normalized.includes('pull') || normalized.includes('leg')) return 'Push/Pull/Legs';
|
|
if (normalized.includes('upper') || normalized.includes('lower')) return 'Upper/Lower';
|
|
if (normalized.includes('cardio')) return 'Cardio';
|
|
if (normalized.includes('crossfit')) return 'CrossFit';
|
|
if (normalized.includes('strength')) return 'Strength';
|
|
if (normalized.includes('condition')) return 'Conditioning';
|
|
|
|
// Default if no match
|
|
return 'Custom';
|
|
}
|
|
|
|
// Add this updated method to the NostrIntegration class
|
|
|
|
getTemplateExerciseRefs(templateEvent: NDKEvent): string[] {
|
|
const exerciseTags = templateEvent.getMatchingTags('exercise');
|
|
const exerciseRefs: string[] = [];
|
|
|
|
for (const tag of exerciseTags) {
|
|
if (tag.length < 2) continue;
|
|
|
|
let ref = tag[1];
|
|
|
|
// Build a complete reference that includes relay hints
|
|
const relayHints: string[] = [];
|
|
|
|
// Check for relay hints in the main reference (if it has commas)
|
|
if (ref.includes(',')) {
|
|
const [baseRef, ...hints] = ref.split(',');
|
|
ref = baseRef;
|
|
hints.filter(h => h.startsWith('wss://')).forEach(h => relayHints.push(h));
|
|
}
|
|
|
|
// Also check for relay hints in the tag itself (additional elements)
|
|
for (let i = 2; i < tag.length; i++) {
|
|
if (tag[i].startsWith('wss://')) {
|
|
relayHints.push(tag[i]);
|
|
}
|
|
}
|
|
|
|
// Add parameters if available
|
|
let fullRef = ref;
|
|
|
|
// Check if params start after tag[1]
|
|
if (tag.length > 2 && !tag[2].startsWith('wss://')) {
|
|
let paramStart = 2;
|
|
|
|
// Find all non-relay parameters
|
|
const params: string[] = [];
|
|
for (let i = paramStart; i < tag.length; i++) {
|
|
if (!tag[i].startsWith('wss://')) {
|
|
params.push(tag[i]);
|
|
}
|
|
}
|
|
|
|
if (params.length > 0) {
|
|
// Add parameters with "::" separator
|
|
fullRef += `::${params.join(':')}`;
|
|
}
|
|
}
|
|
|
|
// Reconstruct the reference with relay hints
|
|
if (relayHints.length > 0) {
|
|
fullRef += `,${relayHints.join(',')}`;
|
|
}
|
|
|
|
exerciseRefs.push(fullRef);
|
|
console.log(`Extracted reference from template: ${fullRef}`);
|
|
}
|
|
|
|
return exerciseRefs;
|
|
}
|
|
|
|
// Add this updated method to the NostrIntegration class
|
|
|
|
async findExercisesByNostrReference(refs: string[]): Promise<Map<string, string>> {
|
|
try {
|
|
const result = new Map<string, string>();
|
|
|
|
for (const ref of refs) {
|
|
// Split the reference to separate the base reference from relay hints
|
|
const [baseRefWithParams, ...relayHints] = ref.split(',');
|
|
|
|
// Further split to get the basic reference and parameters
|
|
let baseRef = baseRefWithParams;
|
|
if (baseRefWithParams.includes('::')) {
|
|
baseRef = baseRefWithParams.split('::')[0];
|
|
}
|
|
|
|
const refParts = baseRef.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]
|
|
);
|
|
}
|
|
|
|
// If not found, try matching by Nostr event ID
|
|
if (!exercise) {
|
|
exercise = await this.db.getFirstAsync<{ id: string }>(
|
|
`SELECT id FROM exercises WHERE nostr_event_id = ?`,
|
|
[refDTag]
|
|
);
|
|
}
|
|
|
|
// If still not found, try a direct ID match (in case dTag is an 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}`);
|
|
|
|
// Also store the base reference for easier future lookup
|
|
result.set(baseRef, exercise.id);
|
|
}
|
|
}
|
|
|
|
return result;
|
|
} catch (error) {
|
|
console.error('Error finding exercises by Nostr reference:', error);
|
|
return new Map();
|
|
}
|
|
}
|
|
|
|
// Add this method to the NostrIntegration class
|
|
|
|
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);
|
|
|
|
// Get relay hints from the event if available
|
|
const relayHints: string[] = [];
|
|
if (originalEvent) {
|
|
originalEvent.getMatchingTags('r').forEach(tag => {
|
|
if (tag.length > 1 && tag[1].startsWith('wss://')) {
|
|
relayHints.push(tag[1]);
|
|
}
|
|
});
|
|
}
|
|
|
|
// Store the d-tag and relay hints in metadata
|
|
const nostrMetadata = JSON.stringify({
|
|
pubkey: originalEvent?.pubkey || exercise.availability?.lastSynced?.nostr?.metadata?.pubkey,
|
|
dTag: dTag,
|
|
eventId: nostrEventId,
|
|
relays: relayHints
|
|
});
|
|
|
|
// 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${hasNostrMetadata ? ', nostr_metadata' : ''})
|
|
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?${hasNostrMetadata ? ', ?' : ''})`,
|
|
[
|
|
exercise.id,
|
|
exercise.title,
|
|
exercise.type,
|
|
exercise.category,
|
|
exercise.equipment || 'other',
|
|
exercise.description || '',
|
|
formatJson,
|
|
formatUnitsJson,
|
|
exercise.created_at,
|
|
Date.now(),
|
|
'nostr',
|
|
nostrEventId,
|
|
...(hasNostrMetadata ? [nostrMetadata] : [])
|
|
]
|
|
);
|
|
|
|
// Save tags
|
|
if (exercise.tags && exercise.tags.length > 0) {
|
|
for (const tag of exercise.tags) {
|
|
await this.db.runAsync(
|
|
`INSERT INTO exercise_tags (exercise_id, tag) VALUES (?, ?)`,
|
|
[exercise.id, tag]
|
|
);
|
|
}
|
|
}
|
|
|
|
return exercise.id;
|
|
} catch (error) {
|
|
console.error('Error saving imported exercise:', error);
|
|
throw error;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Save an imported template to the database
|
|
*/
|
|
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, author_pubkey, is_archived${hasNostrMetadata ? ', nostr_metadata' : ''})
|
|
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?${hasNostrMetadata ? ', ?' : ''})`,
|
|
[
|
|
template.id,
|
|
template.title,
|
|
template.type,
|
|
template.description || '',
|
|
template.created_at,
|
|
template.lastUpdated || Date.now(),
|
|
'nostr',
|
|
template.nostrEventId || null,
|
|
template.authorPubkey || null,
|
|
template.isArchived ? 1 : 0,
|
|
...(hasNostrMetadata ? [nostrMetadata] : [])
|
|
]
|
|
);
|
|
|
|
return template.id;
|
|
} catch (error) {
|
|
console.error('Error saving imported template:', error);
|
|
throw error;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Save template exercise relationships
|
|
*/
|
|
async saveTemplateExercisesWithParams(
|
|
templateId: string,
|
|
exerciseIds: string[],
|
|
exerciseRefs: string[]
|
|
): Promise<void> {
|
|
try {
|
|
console.log(`=== SAVING TEMPLATE EXERCISES ===`);
|
|
console.log(`Template ID: ${templateId}`);
|
|
console.log(`Exercise IDs (${exerciseIds.length}):`, exerciseIds);
|
|
console.log(`Exercise Refs (${exerciseRefs.length}):`, exerciseRefs);
|
|
|
|
// 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`);
|
|
}
|
|
|
|
// Check if relay_hints column exists
|
|
const hasRelayHints = await this.columnExists('template_exercises', 'relay_hints');
|
|
|
|
if (!hasRelayHints) {
|
|
console.log("Adding relay_hints column to template_exercises table");
|
|
await this.db.execAsync(`ALTER TABLE template_exercises ADD COLUMN relay_hints TEXT`);
|
|
}
|
|
|
|
// Clear out any existing entries for this template
|
|
await this.db.runAsync(
|
|
`DELETE FROM template_exercises WHERE template_id = ?`,
|
|
[templateId]
|
|
);
|
|
|
|
// Create template exercise records
|
|
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[i] || '';
|
|
console.log(`Processing reference: ${exerciseRef}`);
|
|
|
|
// Extract relay hints from the reference
|
|
const parts = exerciseRef.split(',');
|
|
const baseRefWithParams = parts[0]; // This might include ::params
|
|
const relayHints = parts.slice(1).filter(r => r.startsWith('wss://'));
|
|
|
|
// 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 (baseRefWithParams.includes('::')) {
|
|
const [_, paramString] = baseRefWithParams.split('::');
|
|
const params = paramString.split(':');
|
|
|
|
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}`);
|
|
}
|
|
|
|
// Store relay hints in JSON
|
|
const relayHintsJson = relayHints.length > 0 ? JSON.stringify(relayHints) : null;
|
|
|
|
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
|
|
${hasNostrReference ? ', nostr_reference' : ''}${hasRelayHints ? ', relay_hints' : ''})
|
|
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?${hasNostrReference ? ', ?' : ''}${hasRelayHints ? ', ?' : ''})`,
|
|
[
|
|
templateExerciseId,
|
|
templateId,
|
|
exerciseId,
|
|
i,
|
|
targetSets,
|
|
targetReps,
|
|
targetWeight,
|
|
now,
|
|
now,
|
|
...(hasNostrReference ? [exerciseRef] : []),
|
|
...(hasRelayHints ? [relayHintsJson] : [])
|
|
]
|
|
);
|
|
|
|
console.log(`Saved template-exercise relationship: template=${templateId}, exercise=${exerciseId} with ${relayHints.length} relay hints`);
|
|
}
|
|
|
|
// Verify the relationships were saved
|
|
const savedRelationships = await this.db.getAllAsync<{id: string, exercise_id: string}>(
|
|
`SELECT id, exercise_id FROM template_exercises WHERE template_id = ?`,
|
|
[templateId]
|
|
);
|
|
|
|
console.log(`Saved ${savedRelationships.length} template-exercise relationships for template ${templateId}`);
|
|
savedRelationships.forEach(rel => console.log(` - Exercise ID: ${rel.exercise_id}`));
|
|
console.log(`=== END SAVING TEMPLATE EXERCISES ===`);
|
|
} catch (error) {
|
|
console.error('Error saving template exercises with parameters:', error);
|
|
throw error;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 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;
|
|
}
|
|
}
|
|
} |