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