From b1e9b4a9ca7350cc601f543240c55c475165958d Mon Sep 17 00:00:00 2001 From: Zac Gaetano Date: Tue, 7 Apr 2026 21:58:28 -0400 Subject: [PATCH] add services/mam-api/src/routes/projects.js --- services/mam-api/src/routes/projects.js | 147 ++++++++++++++++++++++++ 1 file changed, 147 insertions(+) create mode 100644 services/mam-api/src/routes/projects.js diff --git a/services/mam-api/src/routes/projects.js b/services/mam-api/src/routes/projects.js new file mode 100644 index 0000000..d4f4ea7 --- /dev/null +++ b/services/mam-api/src/routes/projects.js @@ -0,0 +1,147 @@ +import express from 'express'; +import pool from '../db/pool.js'; +import { requireAuth } from '../middleware/auth.js'; +import { v4 as uuidv4 } from 'uuid'; + +const router = express.Router(); + +router.use(requireAuth); + +// Helper function to slugify +const slugify = (str) => { + return str + .toLowerCase() + .trim() + .replace(/[^\w\s-]/g, '') + .replace(/\s+/g, '-') + .replace(/-+/g, '-'); +}; + +// GET / - List all projects +router.get('/', async (req, res, next) => { + try { + const result = await pool.query('SELECT * FROM projects ORDER BY created_at DESC'); + res.json(result.rows); + } catch (err) { + next(err); + } +}); + +// POST / - Create project +router.post('/', async (req, res, next) => { + try { + const { name, description } = req.body; + + if (!name) { + return res.status(400).json({ error: 'Name is required' }); + } + + const id = uuidv4(); + const s3_prefix = slugify(name); + + const result = await pool.query( + `INSERT INTO projects (id, name, description, s3_prefix, created_at, updated_at) + VALUES ($1, $2, $3, $4, NOW(), NOW()) + RETURNING *`, + [id, name, description || null, s3_prefix] + ); + + res.status(201).json(result.rows[0]); + } catch (err) { + next(err); + } +}); + +// GET /:id - Single project with asset count +router.get('/:id', async (req, res, next) => { + try { + const { id } = req.params; + + const result = await pool.query( + `SELECT p.*, + COUNT(a.id) AS asset_count + FROM projects p + LEFT JOIN assets a ON a.project_id = p.id AND a.status != 'archived' + WHERE p.id = $1 + GROUP BY p.id`, + [id] + ); + + if (result.rows.length === 0) { + return res.status(404).json({ error: 'Project not found' }); + } + + res.json(result.rows[0]); + } catch (err) { + next(err); + } +}); + +// PATCH /:id - Update project +router.patch('/:id', async (req, res, next) => { + try { + const { id } = req.params; + const { name, description } = req.body; + + const updates = []; + const params = []; + let paramCount = 1; + + if (name !== undefined) { + updates.push(`name = $${paramCount++}`); + params.push(name); + } + + if (description !== undefined) { + updates.push(`description = $${paramCount++}`); + params.push(description); + } + + if (updates.length === 0) { + return res.status(400).json({ error: 'No fields to update' }); + } + + updates.push(`updated_at = NOW()`); + params.push(id); + + const query = ` + UPDATE projects + SET ${updates.join(', ')} + WHERE id = $${paramCount} + RETURNING * + `; + + const result = await pool.query(query, params); + + if (result.rows.length === 0) { + return res.status(404).json({ error: 'Project not found' }); + } + + res.json(result.rows[0]); + } catch (err) { + next(err); + } +}); + +// DELETE /:id - Delete project and cascade +router.delete('/:id', async (req, res, next) => { + try { + const { id } = req.params; + + // Delete project (cascade should handle related records) + const result = await pool.query( + 'DELETE FROM projects WHERE id = $1 RETURNING *', + [id] + ); + + if (result.rows.length === 0) { + return res.status(404).json({ error: 'Project not found' }); + } + + res.json({ message: 'Project deleted', project: result.rows[0] }); + } catch (err) { + next(err); + } +}); + +export default router;