feature/05-exercise-encyclopedia #4
@@ -0,0 +1,173 @@
|
|||||||
|
const express = require('express');
|
||||||
|
const pool = require('../db/pool');
|
||||||
|
const router = express.Router();
|
||||||
|
|
||||||
|
// Validation helper
|
||||||
|
const validateExercise = (data) => {
|
||||||
|
const errors = [];
|
||||||
|
if (!data.name || typeof data.name !== 'string' || !data.name.trim()) {
|
||||||
|
errors.push('name is required and must be non-empty');
|
||||||
|
}
|
||||||
|
if (data.difficulty && !['beginner', 'intermediate', 'advanced'].includes(data.difficulty)) {
|
||||||
|
errors.push('difficulty must be beginner, intermediate, or advanced');
|
||||||
|
}
|
||||||
|
if (data.muscle_groups && !Array.isArray(data.muscle_groups)) {
|
||||||
|
errors.push('muscle_groups must be an array');
|
||||||
|
}
|
||||||
|
if (data.equipment_needed && !Array.isArray(data.equipment_needed)) {
|
||||||
|
errors.push('equipment_needed must be an array');
|
||||||
|
}
|
||||||
|
return errors;
|
||||||
|
};
|
||||||
|
|
||||||
|
// CREATE - Add new exercise
|
||||||
|
router.post('/', async (req, res) => {
|
||||||
|
try {
|
||||||
|
const { name, description, instructions, muscle_groups, difficulty, equipment_needed, video_url, created_by } = req.body;
|
||||||
|
|
||||||
|
const errors = validateExercise({ name, difficulty, muscle_groups, equipment_needed });
|
||||||
|
if (errors.length > 0) {
|
||||||
|
return res.status(400).json({ error: 'Validation failed', details: errors });
|
||||||
|
}
|
||||||
|
|
||||||
|
const query = `
|
||||||
|
INSERT INTO exercises (name, description, instructions, muscle_groups, difficulty, equipment_needed, video_url, created_by)
|
||||||
|
VALUES ($1, $2, $3, $4, $5, $6, $7, $8)
|
||||||
|
RETURNING *
|
||||||
|
`;
|
||||||
|
|
||||||
|
const result = await pool.query(query, [
|
||||||
|
name.trim(),
|
||||||
|
description || null,
|
||||||
|
instructions || null,
|
||||||
|
muscle_groups || [],
|
||||||
|
difficulty || 'intermediate',
|
||||||
|
equipment_needed || [],
|
||||||
|
video_url || null,
|
||||||
|
created_by || 'system'
|
||||||
|
]);
|
||||||
|
|
||||||
|
res.status(201).json(result.rows[0]);
|
||||||
|
} catch (err) {
|
||||||
|
if (err.code === '23505') {
|
||||||
|
return res.status(409).json({ error: 'Exercise name already exists' });
|
||||||
|
}
|
||||||
|
console.error('Error creating exercise:', err);
|
||||||
|
res.status(500).json({ error: 'Failed to create exercise' });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// READ - Get all exercises with search/filter
|
||||||
|
router.get('/', async (req, res) => {
|
||||||
|
try {
|
||||||
|
const { search, difficulty, muscle_group, limit = 50, offset = 0 } = req.query;
|
||||||
|
|
||||||
|
let query = 'SELECT * FROM exercises WHERE 1=1';
|
||||||
|
const params = [];
|
||||||
|
let paramCount = 1;
|
||||||
|
|
||||||
|
if (search) {
|
||||||
|
query += ` AND (name ILIKE $${paramCount} OR description ILIKE $${paramCount})`;
|
||||||
|
params.push(`%${search}%`);
|
||||||
|
paramCount++;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (difficulty) {
|
||||||
|
query += ` AND difficulty = $${paramCount}`;
|
||||||
|
params.push(difficulty);
|
||||||
|
paramCount++;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (muscle_group) {
|
||||||
|
query += ` AND $${paramCount} = ANY(muscle_groups)`;
|
||||||
|
params.push(muscle_group);
|
||||||
|
paramCount++;
|
||||||
|
}
|
||||||
|
|
||||||
|
query += ` ORDER BY name ASC LIMIT $${paramCount} OFFSET $${paramCount + 1}`;
|
||||||
|
params.push(parseInt(limit), parseInt(offset));
|
||||||
|
|
||||||
|
const result = await pool.query(query, params);
|
||||||
|
res.json(result.rows);
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Error fetching exercises:', err);
|
||||||
|
res.status(500).json({ error: 'Failed to fetch exercises' });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// READ - Get single exercise
|
||||||
|
router.get('/:id', async (req, res) => {
|
||||||
|
try {
|
||||||
|
const result = await pool.query('SELECT * FROM exercises WHERE id = $1', [req.params.id]);
|
||||||
|
|
||||||
|
if (result.rows.length === 0) {
|
||||||
|
return res.status(404).json({ error: 'Exercise not found' });
|
||||||
|
}
|
||||||
|
|
||||||
|
res.json(result.rows[0]);
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Error fetching exercise:', err);
|
||||||
|
res.status(500).json({ error: 'Failed to fetch exercise' });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// UPDATE - Modify exercise
|
||||||
|
router.put('/:id', async (req, res) => {
|
||||||
|
try {
|
||||||
|
const { name, description, instructions, muscle_groups, difficulty, equipment_needed, video_url } = req.body;
|
||||||
|
|
||||||
|
const errors = validateExercise({ name, difficulty, muscle_groups, equipment_needed });
|
||||||
|
if (errors.length > 0) {
|
||||||
|
return res.status(400).json({ error: 'Validation failed', details: errors });
|
||||||
|
}
|
||||||
|
|
||||||
|
const query = `
|
||||||
|
UPDATE exercises
|
||||||
|
SET name = $1, description = $2, instructions = $3, muscle_groups = $4,
|
||||||
|
difficulty = $5, equipment_needed = $6, video_url = $7, updated_at = CURRENT_TIMESTAMP
|
||||||
|
WHERE id = $8
|
||||||
|
RETURNING *
|
||||||
|
`;
|
||||||
|
|
||||||
|
const result = await pool.query(query, [
|
||||||
|
name.trim(),
|
||||||
|
description || null,
|
||||||
|
instructions || null,
|
||||||
|
muscle_groups || [],
|
||||||
|
difficulty || 'intermediate',
|
||||||
|
equipment_needed || [],
|
||||||
|
video_url || null,
|
||||||
|
req.params.id
|
||||||
|
]);
|
||||||
|
|
||||||
|
if (result.rows.length === 0) {
|
||||||
|
return res.status(404).json({ error: 'Exercise not found' });
|
||||||
|
}
|
||||||
|
|
||||||
|
res.json(result.rows[0]);
|
||||||
|
} catch (err) {
|
||||||
|
if (err.code === '23505') {
|
||||||
|
return res.status(409).json({ error: 'Exercise name already exists' });
|
||||||
|
}
|
||||||
|
console.error('Error updating exercise:', err);
|
||||||
|
res.status(500).json({ error: 'Failed to update exercise' });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// DELETE - Remove exercise
|
||||||
|
router.delete('/:id', async (req, res) => {
|
||||||
|
try {
|
||||||
|
const result = await pool.query('DELETE FROM exercises WHERE id = $1 RETURNING *', [req.params.id]);
|
||||||
|
|
||||||
|
if (result.rows.length === 0) {
|
||||||
|
return res.status(404).json({ error: 'Exercise not found' });
|
||||||
|
}
|
||||||
|
|
||||||
|
res.json({ message: 'Exercise deleted', id: req.params.id });
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Error deleting exercise:', err);
|
||||||
|
res.status(500).json({ error: 'Failed to delete exercise' });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
module.exports = router;
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
-- Create exercises table for exercise encyclopedia
|
||||||
|
CREATE TABLE IF NOT EXISTS exercises (
|
||||||
|
id SERIAL PRIMARY KEY,
|
||||||
|
name VARCHAR(255) NOT NULL UNIQUE,
|
||||||
|
description TEXT,
|
||||||
|
instructions TEXT,
|
||||||
|
muscle_groups TEXT[] DEFAULT ARRAY[]::text[],
|
||||||
|
difficulty VARCHAR(20) DEFAULT 'intermediate' CHECK (difficulty IN ('beginner', 'intermediate', 'advanced')),
|
||||||
|
equipment_needed TEXT[] DEFAULT ARRAY[]::text[],
|
||||||
|
video_url VARCHAR(255),
|
||||||
|
created_by VARCHAR(50),
|
||||||
|
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE INDEX idx_exercises_name ON exercises(name);
|
||||||
|
CREATE INDEX idx_exercises_difficulty ON exercises(difficulty);
|
||||||
|
CREATE INDEX idx_exercises_muscle_groups ON exercises USING GIN(muscle_groups);
|
||||||
Reference in New Issue
Block a user