POWR/lib/db/services/ExerciseService.ts

456 lines
14 KiB
TypeScript
Raw Normal View History

2025-02-16 23:53:28 -05:00
// lib/db/services/ExerciseService.ts
import { SQLiteDatabase } from 'expo-sqlite';
2025-02-19 21:39:47 -05:00
import {
BaseExercise,
ExerciseDisplay,
ExerciseType,
ExerciseCategory,
Equipment,
toExerciseDisplay
} from '@/types/exercise';
2025-02-16 23:53:28 -05:00
import { generateId } from '@/utils/ids';
export class ExerciseService {
constructor(private db: SQLiteDatabase) {}
2025-02-19 21:39:47 -05:00
async getAllExercises(): Promise<ExerciseDisplay[]> {
2025-02-16 23:53:28 -05:00
try {
const exercises = await this.db.getAllAsync<any>(`
SELECT * FROM exercises ORDER BY created_at DESC
`);
const exerciseIds = exercises.map(e => e.id);
if (exerciseIds.length === 0) return [];
const tags = await this.db.getAllAsync<{ exercise_id: string; tag: string }>(
`SELECT exercise_id, tag
FROM exercise_tags
WHERE exercise_id IN (${exerciseIds.map(() => '?').join(',')})`,
exerciseIds
);
const tagsByExercise = tags.reduce((acc, { exercise_id, tag }) => {
acc[exercise_id] = acc[exercise_id] || [];
acc[exercise_id].push(tag);
return acc;
}, {} as Record<string, string[]>);
2025-02-19 21:39:47 -05:00
return exercises.map(exercise => {
const baseExercise: BaseExercise = {
...exercise,
tags: tagsByExercise[exercise.id] || [],
format: exercise.format_json ? JSON.parse(exercise.format_json) : undefined,
format_units: exercise.format_units_json ? JSON.parse(exercise.format_units_json) : undefined,
availability: { source: [exercise.source] }
};
return toExerciseDisplay(baseExercise);
});
} catch (error) {
console.error('Error getting all exercises:', error);
throw error;
}
}
async getExercise(id: string): Promise<ExerciseDisplay | null> {
try {
// Get exercise data
const exercise = await this.db.getFirstAsync<any>(
`SELECT * FROM exercises WHERE id = ?`,
[id]
);
if (!exercise) return null;
// Get tags
const tags = await this.db.getAllAsync<{ tag: string }>(
'SELECT tag FROM exercise_tags WHERE exercise_id = ?',
[id]
);
const baseExercise: BaseExercise = {
2025-02-16 23:53:28 -05:00
...exercise,
format: exercise.format_json ? JSON.parse(exercise.format_json) : undefined,
format_units: exercise.format_units_json ? JSON.parse(exercise.format_units_json) : undefined,
2025-02-19 21:39:47 -05:00
tags: tags.map(t => t.tag),
2025-02-16 23:53:28 -05:00
availability: { source: [exercise.source] }
2025-02-19 21:39:47 -05:00
};
return toExerciseDisplay(baseExercise);
2025-02-16 23:53:28 -05:00
} catch (error) {
2025-02-19 21:39:47 -05:00
console.error('Error getting exercise:', error);
2025-02-16 23:53:28 -05:00
throw error;
}
}
async createExercise(
2025-02-19 21:39:47 -05:00
exercise: Omit<BaseExercise, 'id'>,
inTransaction: boolean = false
): Promise<string> {
2025-02-16 23:53:28 -05:00
const id = generateId();
const timestamp = Date.now();
2025-02-19 21:39:47 -05:00
2025-02-16 23:53:28 -05:00
try {
const runQueries = async () => {
2025-02-16 23:53:28 -05:00
await this.db.runAsync(
`INSERT INTO exercises (
id, title, type, category, equipment, description,
format_json, format_units_json, created_at, updated_at, source
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
[
id,
exercise.title,
exercise.type,
exercise.category,
exercise.equipment || null,
exercise.description || null,
exercise.format ? JSON.stringify(exercise.format) : null,
exercise.format_units ? JSON.stringify(exercise.format_units) : null,
timestamp,
timestamp,
2025-02-19 21:39:47 -05:00
exercise.availability.source[0]
2025-02-16 23:53:28 -05:00
]
);
2025-02-19 21:39:47 -05:00
2025-02-16 23:53:28 -05:00
if (exercise.tags?.length) {
for (const tag of exercise.tags) {
await this.db.runAsync(
'INSERT INTO exercise_tags (exercise_id, tag) VALUES (?, ?)',
[id, tag]
);
}
}
};
2025-02-19 21:39:47 -05:00
if (inTransaction) {
await runQueries();
} else {
await this.db.withTransactionAsync(runQueries);
}
2025-02-19 21:39:47 -05:00
2025-02-16 23:53:28 -05:00
return id;
} catch (error) {
console.error('Error creating exercise:', error);
throw error;
}
}
async searchExercises(query?: string, filters?: {
types?: ExerciseType[];
categories?: ExerciseCategory[];
equipment?: Equipment[];
tags?: string[];
source?: 'local' | 'powr' | 'nostr';
2025-02-19 21:39:47 -05:00
}): Promise<ExerciseDisplay[]> {
2025-02-16 23:53:28 -05:00
try {
let sql = `
SELECT DISTINCT e.*
FROM exercises e
LEFT JOIN exercise_tags et ON e.id = et.exercise_id
WHERE 1=1
`;
const params: any[] = [];
// Add search condition
if (query) {
sql += ' AND (e.title LIKE ? OR e.description LIKE ?)';
params.push(`%${query}%`, `%${query}%`);
}
// Add filter conditions
if (filters?.types?.length) {
sql += ` AND e.type IN (${filters.types.map(() => '?').join(',')})`;
params.push(...filters.types);
}
if (filters?.categories?.length) {
sql += ` AND e.category IN (${filters.categories.map(() => '?').join(',')})`;
params.push(...filters.categories);
}
if (filters?.equipment?.length) {
sql += ` AND e.equipment IN (${filters.equipment.map(() => '?').join(',')})`;
params.push(...filters.equipment);
}
if (filters?.source) {
sql += ' AND e.source = ?';
params.push(filters.source);
}
// Handle tag filtering
if (filters?.tags?.length) {
sql += `
AND e.id IN (
SELECT exercise_id
FROM exercise_tags
WHERE tag IN (${filters.tags.map(() => '?').join(',')})
GROUP BY exercise_id
HAVING COUNT(DISTINCT tag) = ?
)
`;
params.push(...filters.tags, filters.tags.length);
}
// Add ordering
sql += ' ORDER BY e.title ASC';
// Get exercises
2025-02-19 21:39:47 -05:00
const exercises = await this.db.getAllAsync<any>(sql, params);
2025-02-16 23:53:28 -05:00
// Get tags for all exercises
const exerciseIds = exercises.map(e => e.id);
if (exerciseIds.length) {
const tags = await this.db.getAllAsync<{ exercise_id: string; tag: string }>(
`SELECT exercise_id, tag
FROM exercise_tags
WHERE exercise_id IN (${exerciseIds.map(() => '?').join(',')})`,
exerciseIds
);
// Group tags by exercise
const tagsByExercise = tags.reduce((acc, { exercise_id, tag }) => {
acc[exercise_id] = acc[exercise_id] || [];
acc[exercise_id].push(tag);
return acc;
}, {} as Record<string, string[]>);
2025-02-19 21:39:47 -05:00
// Convert to ExerciseDisplay
return exercises.map(exercise => {
const baseExercise: BaseExercise = {
...exercise,
format: exercise.format_json ? JSON.parse(exercise.format_json) : undefined,
format_units: exercise.format_units_json ? JSON.parse(exercise.format_units_json) : undefined,
tags: tagsByExercise[exercise.id] || [],
availability: { source: [exercise.source] }
};
return toExerciseDisplay(baseExercise);
});
2025-02-16 23:53:28 -05:00
}
2025-02-19 21:39:47 -05:00
return [];
2025-02-16 23:53:28 -05:00
} catch (error) {
console.error('Error searching exercises:', error);
throw error;
}
}
2025-02-19 21:39:47 -05:00
async getRecentExercises(limit: number = 10): Promise<ExerciseDisplay[]> {
2025-02-16 23:53:28 -05:00
try {
const sql = `
SELECT e.*
FROM exercises e
ORDER BY e.updated_at DESC
LIMIT ?
`;
2025-02-19 21:39:47 -05:00
const exercises = await this.db.getAllAsync<any>(sql, [limit]);
2025-02-16 23:53:28 -05:00
// Get tags for these exercises
const exerciseIds = exercises.map(e => e.id);
if (exerciseIds.length) {
const tags = await this.db.getAllAsync<{ exercise_id: string; tag: string }>(
`SELECT exercise_id, tag
FROM exercise_tags
WHERE exercise_id IN (${exerciseIds.map(() => '?').join(',')})`,
exerciseIds
);
// Group tags by exercise
const tagsByExercise = tags.reduce((acc, { exercise_id, tag }) => {
acc[exercise_id] = acc[exercise_id] || [];
acc[exercise_id].push(tag);
return acc;
}, {} as Record<string, string[]>);
2025-02-19 21:39:47 -05:00
return exercises.map(exercise => {
const baseExercise: BaseExercise = {
...exercise,
format: exercise.format_json ? JSON.parse(exercise.format_json) : undefined,
format_units: exercise.format_units_json ? JSON.parse(exercise.format_units_json) : undefined,
tags: tagsByExercise[exercise.id] || [],
availability: { source: [exercise.source] }
};
return toExerciseDisplay(baseExercise);
});
2025-02-16 23:53:28 -05:00
}
2025-02-19 21:39:47 -05:00
return [];
2025-02-16 23:53:28 -05:00
} catch (error) {
console.error('Error getting recent exercises:', error);
throw error;
}
}
2025-02-19 21:39:47 -05:00
async updateExercise(id: string, exercise: Partial<BaseExercise>): Promise<void> {
const timestamp = Date.now();
2025-02-16 23:53:28 -05:00
try {
await this.db.withTransactionAsync(async () => {
2025-02-19 21:39:47 -05:00
// Build update query dynamically based on provided fields
const updates: string[] = [];
const values: any[] = [];
if (exercise.title !== undefined) {
updates.push('title = ?');
values.push(exercise.title);
}
if (exercise.type !== undefined) {
updates.push('type = ?');
values.push(exercise.type);
}
if (exercise.category !== undefined) {
updates.push('category = ?');
values.push(exercise.category);
}
if (exercise.equipment !== undefined) {
updates.push('equipment = ?');
values.push(exercise.equipment);
}
if (exercise.description !== undefined) {
updates.push('description = ?');
values.push(exercise.description);
}
if (exercise.format !== undefined) {
updates.push('format_json = ?');
values.push(JSON.stringify(exercise.format));
}
if (exercise.format_units !== undefined) {
updates.push('format_units_json = ?');
values.push(JSON.stringify(exercise.format_units));
}
if (exercise.availability !== undefined) {
updates.push('source = ?');
values.push(exercise.availability.source[0]);
}
if (updates.length > 0) {
updates.push('updated_at = ?');
values.push(timestamp);
// Add id to values array
values.push(id);
await this.db.runAsync(
`UPDATE exercises SET ${updates.join(', ')} WHERE id = ?`,
values
);
}
// Update tags if provided
if (exercise.tags !== undefined) {
// Delete existing tags
await this.db.runAsync(
'DELETE FROM exercise_tags WHERE exercise_id = ?',
[id]
);
// Insert new tags
for (const tag of exercise.tags) {
await this.db.runAsync(
'INSERT INTO exercise_tags (exercise_id, tag) VALUES (?, ?)',
[id, tag]
);
}
2025-02-16 23:53:28 -05:00
}
});
} catch (error) {
2025-02-19 21:39:47 -05:00
console.error('Error updating exercise:', error);
2025-02-16 23:53:28 -05:00
throw error;
}
}
// Add this to lib/db/services/ExerciseService.ts
async deleteExercise(id: string): Promise<boolean> {
try {
// First check if the exercise is from a POWR Pack
const exercise = await this.db.getFirstAsync<{ source: string }>(
'SELECT source FROM exercises WHERE id = ?',
[id]
);
if (!exercise) {
throw new Error(`Exercise with ID ${id} not found`);
}
if (exercise.source === 'nostr' || exercise.source === 'powr') {
// This is a POWR Pack exercise - don't allow direct deletion
throw new Error('This exercise is part of a POWR Pack and cannot be deleted individually. You can remove the entire POWR Pack from the settings menu.');
}
// For local exercises, proceed with deletion
await this.db.runAsync('DELETE FROM exercises WHERE id = ?', [id]);
// Also delete any references in template_exercises
await this.db.runAsync('DELETE FROM template_exercises WHERE exercise_id = ?', [id]);
return true;
} catch (error) {
console.error('Error deleting exercise:', error);
throw error;
}
}
2025-02-19 21:39:47 -05:00
async syncWithNostrEvent(eventId: string, exercise: Omit<BaseExercise, 'id'>): Promise<string> {
2025-02-16 23:53:28 -05:00
try {
// Check if we already have this exercise
const existing = await this.db.getFirstAsync<{ id: string }>(
'SELECT id FROM exercises WHERE nostr_event_id = ?',
[eventId]
);
if (existing) {
// Update existing exercise
await this.updateExercise(existing.id, exercise);
return existing.id;
} else {
// Create new exercise with Nostr reference
const id = generateId();
const timestamp = Date.now();
await this.db.withTransactionAsync(async () => {
await this.db.runAsync(
`INSERT INTO exercises (
id, nostr_event_id, title, type, category, equipment, description,
format_json, format_units_json, created_at, updated_at, source
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
[
id,
eventId,
exercise.title,
exercise.type,
exercise.category,
exercise.equipment || null,
exercise.description || null,
exercise.format ? JSON.stringify(exercise.format) : null,
exercise.format_units ? JSON.stringify(exercise.format_units) : null,
timestamp,
timestamp,
'nostr'
]
);
if (exercise.tags?.length) {
for (const tag of exercise.tags) {
await this.db.runAsync(
'INSERT INTO exercise_tags (exercise_id, tag) VALUES (?, ?)',
[id, tag]
);
}
}
});
return id;
}
} catch (error) {
console.error('Error syncing exercise with Nostr event:', error);
throw error;
}
}
2025-02-19 21:39:47 -05:00
}