mirror of
https://github.com/DocNR/POWR.git
synced 2025-04-23 01:01:27 +00:00
314 lines
9.6 KiB
TypeScript
314 lines
9.6 KiB
TypeScript
![]() |
// services/LibraryService.ts
|
||
|
import { DbService } from '@/utils/db/db-service';
|
||
|
import { generateId } from '@/utils/ids';
|
||
|
import {
|
||
|
BaseExercise,
|
||
|
ExerciseCategory,
|
||
|
Equipment,
|
||
|
LibraryContent
|
||
|
} from '@/types/exercise';
|
||
|
import { WorkoutTemplate } from '@/types/workout';
|
||
|
|
||
|
class LibraryService {
|
||
|
private db: DbService;
|
||
|
|
||
|
constructor() {
|
||
|
this.db = new DbService('powr.db');
|
||
|
}
|
||
|
|
||
|
async addExercise(exercise: Omit<BaseExercise, 'id' | 'created_at' | 'availability'>): Promise<string> {
|
||
|
const id = generateId();
|
||
|
const timestamp = Date.now();
|
||
|
|
||
|
try {
|
||
|
await this.db.withTransaction(async () => {
|
||
|
// Insert main exercise data
|
||
|
await this.db.executeWrite(
|
||
|
`INSERT INTO exercises (
|
||
|
id, title, type, category, equipment, description, created_at, updated_at
|
||
|
) VALUES (?, ?, ?, ?, ?, ?, ?, ?)`,
|
||
|
[
|
||
|
id,
|
||
|
exercise.title,
|
||
|
exercise.type,
|
||
|
exercise.category,
|
||
|
exercise.equipment || null,
|
||
|
exercise.description || null,
|
||
|
timestamp,
|
||
|
timestamp
|
||
|
]
|
||
|
);
|
||
|
|
||
|
// Insert instructions if provided
|
||
|
if (exercise.instructions?.length) {
|
||
|
for (const [index, instruction] of exercise.instructions.entries()) {
|
||
|
await this.db.executeWrite(
|
||
|
`INSERT INTO exercise_instructions (
|
||
|
exercise_id, instruction, display_order
|
||
|
) VALUES (?, ?, ?)`,
|
||
|
[id, instruction, index]
|
||
|
);
|
||
|
}
|
||
|
}
|
||
|
|
||
|
// Insert tags if provided
|
||
|
if (exercise.tags?.length) {
|
||
|
for (const tag of exercise.tags) {
|
||
|
await this.db.executeWrite(
|
||
|
`INSERT INTO exercise_tags (exercise_id, tag) VALUES (?, ?)`,
|
||
|
[id, tag]
|
||
|
);
|
||
|
}
|
||
|
}
|
||
|
|
||
|
// Insert format settings if provided
|
||
|
if (exercise.format) {
|
||
|
await this.db.executeWrite(
|
||
|
`INSERT INTO exercise_format (
|
||
|
exercise_id, format_json, units_json
|
||
|
) VALUES (?, ?, ?)`,
|
||
|
[
|
||
|
id,
|
||
|
JSON.stringify(exercise.format),
|
||
|
JSON.stringify(exercise.format_units || {})
|
||
|
]
|
||
|
);
|
||
|
}
|
||
|
});
|
||
|
|
||
|
return id;
|
||
|
} catch (error) {
|
||
|
console.error('Error adding exercise:', error);
|
||
|
throw error;
|
||
|
}
|
||
|
}
|
||
|
|
||
|
async getExercise(id: string): Promise<BaseExercise | null> {
|
||
|
try {
|
||
|
const result = await this.db.executeSql(
|
||
|
`SELECT
|
||
|
e.*,
|
||
|
GROUP_CONCAT(DISTINCT i.instruction) as instructions,
|
||
|
GROUP_CONCAT(DISTINCT t.tag) as tags,
|
||
|
f.format_json,
|
||
|
f.units_json
|
||
|
FROM exercises e
|
||
|
LEFT JOIN exercise_instructions i ON e.id = i.exercise_id
|
||
|
LEFT JOIN exercise_tags t ON e.id = t.exercise_id
|
||
|
LEFT JOIN exercise_format f ON e.id = f.exercise_id
|
||
|
WHERE e.id = ?
|
||
|
GROUP BY e.id`,
|
||
|
[id]
|
||
|
);
|
||
|
|
||
|
if (result.rows.length === 0) return null;
|
||
|
|
||
|
const row = result.rows.item(0);
|
||
|
return {
|
||
|
id: row.id,
|
||
|
title: row.title,
|
||
|
type: row.type,
|
||
|
category: row.category as ExerciseCategory,
|
||
|
equipment: row.equipment as Equipment,
|
||
|
description: row.description,
|
||
|
instructions: row.instructions ? row.instructions.split(',') : [],
|
||
|
tags: row.tags ? row.tags.split(',') : [],
|
||
|
format: row.format_json ? JSON.parse(row.format_json) : undefined,
|
||
|
format_units: row.units_json ? JSON.parse(row.units_json) : undefined,
|
||
|
created_at: row.created_at,
|
||
|
updated_at: row.updated_at,
|
||
|
availability: {
|
||
|
source: ['local']
|
||
|
}
|
||
|
};
|
||
|
} catch (error) {
|
||
|
console.error('Error getting exercise:', error);
|
||
|
throw error;
|
||
|
}
|
||
|
}
|
||
|
|
||
|
async getExercises(category?: ExerciseCategory): Promise<BaseExercise[]> {
|
||
|
try {
|
||
|
const query = `
|
||
|
SELECT
|
||
|
e.*,
|
||
|
GROUP_CONCAT(DISTINCT i.instruction) as instructions,
|
||
|
GROUP_CONCAT(DISTINCT t.tag) as tags,
|
||
|
f.format_json,
|
||
|
f.units_json
|
||
|
FROM exercises e
|
||
|
LEFT JOIN exercise_instructions i ON e.id = i.exercise_id
|
||
|
LEFT JOIN exercise_tags t ON e.id = t.exercise_id
|
||
|
LEFT JOIN exercise_format f ON e.id = f.exercise_id
|
||
|
${category ? 'WHERE e.category = ?' : ''}
|
||
|
GROUP BY e.id
|
||
|
ORDER BY e.title
|
||
|
`;
|
||
|
|
||
|
const result = await this.db.executeSql(query, category ? [category] : []);
|
||
|
|
||
|
return Array.from({ length: result.rows.length }, (_, i) => {
|
||
|
const row = result.rows.item(i);
|
||
|
return {
|
||
|
id: row.id,
|
||
|
title: row.title,
|
||
|
type: row.type,
|
||
|
category: row.category as ExerciseCategory,
|
||
|
equipment: row.equipment as Equipment,
|
||
|
description: row.description,
|
||
|
instructions: row.instructions ? row.instructions.split(',') : [],
|
||
|
tags: row.tags ? row.tags.split(',') : [],
|
||
|
format: row.format_json ? JSON.parse(row.format_json) : undefined,
|
||
|
format_units: row.units_json ? JSON.parse(row.units_json) : undefined,
|
||
|
created_at: row.created_at,
|
||
|
updated_at: row.updated_at,
|
||
|
availability: {
|
||
|
source: ['local']
|
||
|
}
|
||
|
};
|
||
|
});
|
||
|
} catch (error) {
|
||
|
console.error('Error getting exercises:', error);
|
||
|
throw error;
|
||
|
}
|
||
|
}
|
||
|
|
||
|
async getTemplates(): Promise<LibraryContent[]> {
|
||
|
try {
|
||
|
// First get exercises
|
||
|
const exercises = await this.getExercises();
|
||
|
const exerciseContent = exercises.map(exercise => ({
|
||
|
id: exercise.id,
|
||
|
title: exercise.title,
|
||
|
type: 'exercise' as const,
|
||
|
description: exercise.description,
|
||
|
category: exercise.category,
|
||
|
equipment: exercise.equipment,
|
||
|
source: 'local' as const,
|
||
|
tags: exercise.tags,
|
||
|
created_at: exercise.created_at,
|
||
|
availability: {
|
||
|
source: ['local']
|
||
|
}
|
||
|
}));
|
||
|
|
||
|
// Get workout templates
|
||
|
const templatesQuery = `
|
||
|
SELECT
|
||
|
t.*,
|
||
|
GROUP_CONCAT(tt.tag) as tags
|
||
|
FROM templates t
|
||
|
LEFT JOIN template_tags tt ON t.id = tt.template_id
|
||
|
GROUP BY t.id
|
||
|
ORDER BY t.updated_at DESC
|
||
|
`;
|
||
|
|
||
|
const result = await this.db.executeSql(templatesQuery);
|
||
|
const templateContent: LibraryContent[] = Array.from({ length: result.rows.length }, (_, i) => {
|
||
|
const row = result.rows.item(i);
|
||
|
return {
|
||
|
id: row.id,
|
||
|
title: row.title,
|
||
|
type: 'workout' as const,
|
||
|
description: row.description,
|
||
|
author: row.author_name ? {
|
||
|
name: row.author_name,
|
||
|
pubkey: row.author_pubkey
|
||
|
} : undefined,
|
||
|
source: row.source as 'local' | 'pow' | 'nostr',
|
||
|
tags: row.tags ? row.tags.split(',') : [],
|
||
|
created_at: row.created_at,
|
||
|
availability: JSON.parse(row.availability_json)
|
||
|
};
|
||
|
});
|
||
|
|
||
|
return [...exerciseContent, ...templateContent];
|
||
|
} catch (error) {
|
||
|
console.error('Error getting templates:', error);
|
||
|
throw error;
|
||
|
}
|
||
|
}
|
||
|
|
||
|
async getTemplate(id: string): Promise<WorkoutTemplate | null> {
|
||
|
try {
|
||
|
// Get main template data
|
||
|
const templateResult = await this.db.executeSql(
|
||
|
`SELECT t.*, GROUP_CONCAT(tt.tag) as tags
|
||
|
FROM templates t
|
||
|
LEFT JOIN template_tags tt ON t.id = tt.template_id
|
||
|
WHERE t.id = ?
|
||
|
GROUP BY t.id`,
|
||
|
[id]
|
||
|
);
|
||
|
|
||
|
if (templateResult.rows.length === 0) return null;
|
||
|
|
||
|
const templateRow = templateResult.rows.item(0);
|
||
|
const exercises = await this.getTemplateExercises(id);
|
||
|
|
||
|
return {
|
||
|
id: templateRow.id,
|
||
|
title: templateRow.title,
|
||
|
type: templateRow.type,
|
||
|
category: templateRow.category,
|
||
|
description: templateRow.description,
|
||
|
author: templateRow.author_name ? {
|
||
|
name: templateRow.author_name,
|
||
|
pubkey: templateRow.author_pubkey
|
||
|
} : undefined,
|
||
|
exercises,
|
||
|
tags: templateRow.tags ? templateRow.tags.split(',') : [],
|
||
|
rounds: templateRow.rounds,
|
||
|
duration: templateRow.duration,
|
||
|
interval: templateRow.interval_time,
|
||
|
restBetweenRounds: templateRow.rest_between_rounds,
|
||
|
isPublic: Boolean(templateRow.is_public),
|
||
|
created_at: templateRow.created_at,
|
||
|
metadata: templateRow.metadata_json ? JSON.parse(templateRow.metadata_json) : undefined,
|
||
|
availability: JSON.parse(templateRow.availability_json)
|
||
|
};
|
||
|
} catch (error) {
|
||
|
console.error('Error getting template:', error);
|
||
|
throw error;
|
||
|
}
|
||
|
}
|
||
|
|
||
|
private async getTemplateExercises(templateId: string): Promise<Array<{
|
||
|
exercise: BaseExercise;
|
||
|
targetSets?: number;
|
||
|
targetReps?: number;
|
||
|
notes?: string;
|
||
|
}>> {
|
||
|
try {
|
||
|
const result = await this.db.executeSql(
|
||
|
`SELECT te.*, e.*
|
||
|
FROM template_exercises te
|
||
|
JOIN exercises e ON te.exercise_id = e.id
|
||
|
WHERE te.template_id = ?
|
||
|
ORDER BY te.display_order`,
|
||
|
[templateId]
|
||
|
);
|
||
|
|
||
|
return Promise.all(
|
||
|
Array.from({ length: result.rows.length }, async (_, i) => {
|
||
|
const row = result.rows.item(i);
|
||
|
const exercise = await this.getExercise(row.exercise_id);
|
||
|
if (!exercise) throw new Error(`Exercise ${row.exercise_id} not found`);
|
||
|
|
||
|
return {
|
||
|
exercise,
|
||
|
targetSets: row.target_sets,
|
||
|
targetReps: row.target_reps,
|
||
|
notes: row.notes
|
||
|
};
|
||
|
})
|
||
|
);
|
||
|
} catch (error) {
|
||
|
console.error('Error getting template exercises:', error);
|
||
|
throw error;
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
// Export a singleton instance
|
||
|
export const libraryService = new LibraryService();
|