diff --git a/services/mam-api/src/routes/assets.js b/services/mam-api/src/routes/assets.js new file mode 100644 index 0000000..9bf1dfd --- /dev/null +++ b/services/mam-api/src/routes/assets.js @@ -0,0 +1,245 @@ +import express from 'express'; +import pool from '../db/pool.js'; +import { getSignedUrlForObject, deleteObject } from '../s3/client.js'; +import { requireAuth } from '../middleware/auth.js'; + +const router = express.Router(); + +router.use(requireAuth); + +// GET / - List assets with filtering +router.get('/', async (req, res, next) => { + try { + const { + project_id, + bin_id, + status, + search, + media_type, + limit = 50, + offset = 0, + } = req.query; + + let query = ` + SELECT a.*, + COUNT(*) OVER() AS full_count + FROM assets a + WHERE 1=1 + `; + const params = []; + let paramCount = 1; + + if (project_id) { + query += ` AND a.project_id = $${paramCount++}`; + params.push(project_id); + } + + if (bin_id) { + query += ` AND a.bin_id = $${paramCount++}`; + params.push(bin_id); + } + + if (status) { + query += ` AND a.status = $${paramCount++}`; + params.push(status); + } + + if (media_type) { + query += ` AND a.media_type = $${paramCount++}`; + params.push(media_type); + } + + if (search) { + query += ` AND (a.display_name ILIKE $${paramCount} OR a.notes ILIKE $${paramCount})`; + params.push(`%${search}%`); + paramCount++; + } + + query += ` ORDER BY a.created_at DESC`; + query += ` LIMIT $${paramCount++} OFFSET $${paramCount++}`; + params.push(limit, offset); + + const result = await pool.query(query, params); + const total = result.rows.length > 0 ? result.rows[0].full_count : 0; + + res.json({ + assets: result.rows, + total, + }); + } catch (err) { + next(err); + } +}); + +// GET /:id - Single asset +router.get('/:id', async (req, res, next) => { + try { + const { id } = req.params; + const result = await pool.query('SELECT * FROM assets WHERE id = $1', [id]); + + if (result.rows.length === 0) { + return res.status(404).json({ error: 'Asset not found' }); + } + + res.json(result.rows[0]); + } catch (err) { + next(err); + } +}); + +// PATCH /:id - Update asset +router.patch('/:id', async (req, res, next) => { + try { + const { id } = req.params; + const { display_name, tags, notes } = req.body; + + const updates = []; + const params = []; + let paramCount = 1; + + if (display_name !== undefined) { + updates.push(`display_name = $${paramCount++}`); + params.push(display_name); + } + + if (tags !== undefined) { + updates.push(`tags = $${paramCount++}`); + params.push(tags); + } + + if (notes !== undefined) { + updates.push(`notes = $${paramCount++}`); + params.push(notes); + } + + 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 assets + 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: 'Asset not found' }); + } + + res.json(result.rows[0]); + } catch (err) { + next(err); + } +}); + +// DELETE /:id - Soft or hard delete +router.delete('/:id', async (req, res, next) => { + try { + const { id } = req.params; + const { hard } = req.query; + + if (hard === 'true') { + // Hard delete: get asset info, delete from S3, delete from DB + const assetResult = await pool.query( + 'SELECT * FROM assets WHERE id = $1', + [id] + ); + + if (assetResult.rows.length === 0) { + return res.status(404).json({ error: 'Asset not found' }); + } + + const asset = assetResult.rows[0]; + + // Delete from S3 + if (asset.proxy_s3_key) { + await deleteObject(asset.proxy_s3_key); + } + if (asset.thumbnail_s3_key) { + await deleteObject(asset.thumbnail_s3_key); + } + if (asset.original_s3_key) { + await deleteObject(asset.original_s3_key); + } + + // Delete from database + await pool.query('DELETE FROM assets WHERE id = $1', [id]); + + res.json({ message: 'Asset deleted permanently' }); + } else { + // Soft delete: set status to archived + const result = await pool.query( + 'UPDATE assets SET status = $1, updated_at = NOW() WHERE id = $2 RETURNING *', + ['archived', id] + ); + + if (result.rows.length === 0) { + return res.status(404).json({ error: 'Asset not found' }); + } + + res.json(result.rows[0]); + } + } catch (err) { + next(err); + } +}); + +// GET /:id/stream - Signed URL for proxy playback +router.get('/:id/stream', async (req, res, next) => { + try { + const { id } = req.params; + const result = await pool.query( + 'SELECT proxy_s3_key FROM assets WHERE id = $1', + [id] + ); + + if (result.rows.length === 0) { + return res.status(404).json({ error: 'Asset not found' }); + } + + const { proxy_s3_key } = result.rows[0]; + + if (!proxy_s3_key) { + return res.status(400).json({ error: 'No proxy available' }); + } + + const url = await getSignedUrlForObject(proxy_s3_key); + res.json({ url }); + } catch (err) { + next(err); + } +}); + +// GET /:id/thumbnail - Signed URL for thumbnail +router.get('/:id/thumbnail', async (req, res, next) => { + try { + const { id } = req.params; + const result = await pool.query( + 'SELECT thumbnail_s3_key FROM assets WHERE id = $1', + [id] + ); + + if (result.rows.length === 0) { + return res.status(404).json({ error: 'Asset not found' }); + } + + const { thumbnail_s3_key } = result.rows[0]; + + if (!thumbnail_s3_key) { + return res.status(400).json({ error: 'No thumbnail available' }); + } + + const url = await getSignedUrlForObject(thumbnail_s3_key); + res.json({ url }); + } catch (err) { + next(err); + } +}); + +export default router;