Governed substrate for autonomous agents: scoped identity (passports), audited actions, MCP workspace. Infra IPs and secrets redacted for public release.
201 lines
5.7 KiB
JavaScript
201 lines
5.7 KiB
JavaScript
/**
|
|
* fitness/workoutController.js — Workouts CRUD
|
|
*/
|
|
|
|
import express from 'express';
|
|
import { dbAll, dbGet, dbRun, sqlNow } from '../../../database/connection.js';
|
|
import { apiLogger } from '../../../utils/logger.js';
|
|
import { success, created, error, badRequest, notFound } from '../../../utils/response.js';
|
|
|
|
const router = express.Router();
|
|
|
|
// =============================================================================
|
|
// WORKOUTS CRUD
|
|
// =============================================================================
|
|
|
|
/**
|
|
* POST /api/v3/fitness/workouts
|
|
* Create a new workout session
|
|
* @swagger
|
|
* /api/v3/fitness/workouts:
|
|
* post:
|
|
* summary: Create a new workout session
|
|
* tags: [Fitness]
|
|
* security:
|
|
* - bearerAuth: []
|
|
* requestBody:
|
|
* required: true
|
|
* content:
|
|
* application/json:
|
|
* schema:
|
|
* $ref: '#/components/schemas/Workout'
|
|
* responses:
|
|
* 201:
|
|
* description: Workout created
|
|
*/
|
|
router.post('/workouts', async (req, res) => {
|
|
try {
|
|
const { space_id, title, description, started_at, ended_at, notes, source = 'manual' } = req.body;
|
|
|
|
// Validation
|
|
if (!space_id) {
|
|
return badRequest(res, 'space_id is required', 'VALIDATION_ERROR');
|
|
}
|
|
|
|
if (!started_at) {
|
|
return badRequest(res, 'started_at is required', 'VALIDATION_ERROR');
|
|
}
|
|
|
|
const result = await dbRun(`
|
|
INSERT INTO fitness_workouts (space_id, title, description, started_at, ended_at, notes, source, created_at, updated_at)
|
|
VALUES (?, ?, ?, ?, ?, ?, ?, ${sqlNow()}, ${sqlNow()})
|
|
`, [space_id, title || null, description || null, started_at, ended_at || null, notes || null, source]);
|
|
|
|
const workout = await dbGet('SELECT * FROM fitness_workouts WHERE id = ?', [result.lastInsertRowid]);
|
|
|
|
apiLogger.info({ workoutId: workout.id, spaceId: space_id }, 'Workout created');
|
|
|
|
return created(res, workout);
|
|
} catch (err) {
|
|
apiLogger.error({ err }, 'POST /fitness/workouts error');
|
|
return error(res, 'CREATE_FAILED', err.message, 500);
|
|
}
|
|
});
|
|
|
|
/**
|
|
* GET /api/v3/fitness/workouts
|
|
* List workouts for a space
|
|
* @swagger
|
|
* /api/v3/fitness/workouts:
|
|
* get:
|
|
* summary: List workouts for a space
|
|
* tags: [Fitness]
|
|
* security:
|
|
* - bearerAuth: []
|
|
* parameters:
|
|
* - in: query
|
|
* name: space_id
|
|
* schema:
|
|
* type: integer
|
|
* - in: query
|
|
* name: limit
|
|
* schema:
|
|
* type: integer
|
|
* responses:
|
|
* 200:
|
|
* description: List of workouts
|
|
*/
|
|
router.get('/workouts', async (req, res) => {
|
|
try {
|
|
const { space_id, limit = 50, offset = 0 } = req.query;
|
|
|
|
let sql = 'SELECT * FROM fitness_workouts';
|
|
const params = [];
|
|
|
|
if (space_id) {
|
|
sql += ' WHERE space_id = ?';
|
|
params.push(space_id);
|
|
}
|
|
|
|
sql += ' ORDER BY started_at DESC LIMIT ? OFFSET ?';
|
|
params.push(parseInt(limit), parseInt(offset));
|
|
|
|
const workouts = await dbAll(sql, params);
|
|
|
|
return success(res, workouts || []);
|
|
} catch (err) {
|
|
apiLogger.error({ err }, 'GET /fitness/workouts error');
|
|
return error(res, 'FETCH_FAILED', err.message, 500);
|
|
}
|
|
});
|
|
|
|
/**
|
|
* GET /api/v3/fitness/workouts/:id
|
|
* Get single workout with its sets
|
|
*/
|
|
router.get('/workouts/:id', async (req, res) => {
|
|
try {
|
|
const { id } = req.params;
|
|
|
|
const workout = await dbGet('SELECT * FROM fitness_workouts WHERE id = ?', [id]);
|
|
|
|
if (!workout) {
|
|
return notFound(res, 'Workout not found');
|
|
}
|
|
|
|
// Get sets for this workout
|
|
const sets = await dbAll(
|
|
'SELECT * FROM fitness_sets WHERE workout_id = ? ORDER BY set_index',
|
|
[id]
|
|
);
|
|
|
|
return success(res, { ...workout, sets: sets || [] });
|
|
} catch (err) {
|
|
apiLogger.error({ err }, 'GET /fitness/workouts/:id error');
|
|
return error(res, 'FETCH_FAILED', err.message, 500);
|
|
}
|
|
});
|
|
|
|
/**
|
|
* PUT /api/v3/fitness/workouts/:id
|
|
* Update a workout
|
|
*/
|
|
router.put('/workouts/:id', async (req, res) => {
|
|
try {
|
|
const { id } = req.params;
|
|
const { title, description, started_at, ended_at, notes } = req.body;
|
|
|
|
// Check workout exists
|
|
const existing = await dbGet('SELECT * FROM fitness_workouts WHERE id = ?', [id]);
|
|
if (!existing) {
|
|
return notFound(res, 'Workout not found');
|
|
}
|
|
|
|
await dbRun(`
|
|
UPDATE fitness_workouts
|
|
SET title = COALESCE(?, title),
|
|
description = COALESCE(?, description),
|
|
started_at = COALESCE(?, started_at),
|
|
ended_at = COALESCE(?, ended_at),
|
|
notes = COALESCE(?, notes),
|
|
updated_at = ${sqlNow()}
|
|
WHERE id = ?
|
|
`, [title, description, started_at, ended_at, notes, id]);
|
|
|
|
const updated = await dbGet('SELECT * FROM fitness_workouts WHERE id = ?', [id]);
|
|
|
|
apiLogger.info({ workoutId: id }, 'Workout updated');
|
|
|
|
return success(res, updated);
|
|
} catch (err) {
|
|
apiLogger.error({ err }, 'PUT /fitness/workouts/:id error');
|
|
return error(res, 'UPDATE_FAILED', err.message, 500);
|
|
}
|
|
});
|
|
|
|
/**
|
|
* DELETE /api/v3/fitness/workouts/:id
|
|
* Delete a workout (cascades to sets)
|
|
*/
|
|
router.delete('/workouts/:id', async (req, res) => {
|
|
try {
|
|
const { id } = req.params;
|
|
|
|
// Check workout exists
|
|
const existing = await dbGet('SELECT * FROM fitness_workouts WHERE id = ?', [id]);
|
|
if (!existing) {
|
|
return notFound(res, 'Workout not found');
|
|
}
|
|
|
|
await dbRun('DELETE FROM fitness_workouts WHERE id = ?', [id]);
|
|
|
|
apiLogger.info({ workoutId: id }, 'Workout deleted');
|
|
|
|
return success(res, { deleted: true, id: parseInt(id) });
|
|
} catch (err) {
|
|
apiLogger.error({ err }, 'DELETE /fitness/workouts/:id error');
|
|
return error(res, 'DELETE_FAILED', err.message, 500);
|
|
}
|
|
});
|
|
|
|
export default router;
|