feat: add POST /sessions/bulk endpoint for bulk archive, unarchive, and delete

Made-with: Cursor
This commit is contained in:
cottongin
2026-03-23 02:15:25 -04:00
parent b40176033f
commit bbd2e51567
2 changed files with 187 additions and 0 deletions

View File

@@ -179,6 +179,62 @@ router.post('/', authenticateToken, (req, res) => {
} }
}); });
// Bulk session operations (admin only)
router.post('/bulk', authenticateToken, (req, res) => {
try {
const { action, ids } = req.body;
if (!Array.isArray(ids) || ids.length === 0) {
return res.status(400).json({ error: 'ids must be a non-empty array' });
}
const validActions = ['archive', 'unarchive', 'delete'];
if (!validActions.includes(action)) {
return res.status(400).json({ error: `action must be one of: ${validActions.join(', ')}` });
}
const placeholders = ids.map(() => '?').join(',');
const sessions = db.prepare(
`SELECT id, is_active FROM sessions WHERE id IN (${placeholders})`
).all(...ids);
if (sessions.length !== ids.length) {
const foundIds = sessions.map(s => s.id);
const missingIds = ids.filter(id => !foundIds.includes(id));
return res.status(404).json({ error: 'Some sessions not found', missingIds });
}
if (action === 'archive' || action === 'delete') {
const activeIds = sessions.filter(s => s.is_active === 1).map(s => s.id);
if (activeIds.length > 0) {
return res.status(400).json({
error: `Cannot ${action} active sessions. Close them first.`,
activeIds
});
}
}
const bulkOperation = db.transaction(() => {
if (action === 'archive') {
db.prepare(`UPDATE sessions SET archived = 1 WHERE id IN (${placeholders})`).run(...ids);
} else if (action === 'unarchive') {
db.prepare(`UPDATE sessions SET archived = 0 WHERE id IN (${placeholders})`).run(...ids);
} else if (action === 'delete') {
db.prepare(`DELETE FROM chat_logs WHERE session_id IN (${placeholders})`).run(...ids);
db.prepare(`DELETE FROM live_votes WHERE session_id IN (${placeholders})`).run(...ids);
db.prepare(`DELETE FROM session_games WHERE session_id IN (${placeholders})`).run(...ids);
db.prepare(`DELETE FROM sessions WHERE id IN (${placeholders})`).run(...ids);
}
});
bulkOperation();
res.json({ success: true, affected: ids.length });
} catch (error) {
res.status(500).json({ error: error.message });
}
});
// Close/finalize session (admin only) // Close/finalize session (admin only)
router.post('/:id/close', authenticateToken, (req, res) => { router.post('/:id/close', authenticateToken, (req, res) => {
try { try {

View File

@@ -171,3 +171,134 @@ describe('POST /api/sessions/:id/unarchive', () => {
expect(res.status).toBe(404); expect(res.status).toBe(404);
}); });
}); });
describe('POST /api/sessions/bulk', () => {
beforeEach(() => {
cleanDb();
});
test('bulk archive multiple sessions', async () => {
const s1 = seedSession({ is_active: 0, notes: null });
const s2 = seedSession({ is_active: 0, notes: null });
const res = await request(app)
.post('/api/sessions/bulk')
.set('Authorization', getAuthHeader())
.send({ action: 'archive', ids: [s1.id, s2.id] });
expect(res.status).toBe(200);
expect(res.body.success).toBe(true);
expect(res.body.affected).toBe(2);
const list = await request(app).get('/api/sessions?filter=archived');
expect(list.body).toHaveLength(2);
});
test('bulk unarchive multiple sessions', async () => {
const s1 = seedSession({ is_active: 0, notes: null });
const s2 = seedSession({ is_active: 0, notes: null });
const db = require('../helpers/test-utils').db;
db.prepare('UPDATE sessions SET archived = 1 WHERE id IN (?, ?)').run(s1.id, s2.id);
const res = await request(app)
.post('/api/sessions/bulk')
.set('Authorization', getAuthHeader())
.send({ action: 'unarchive', ids: [s1.id, s2.id] });
expect(res.status).toBe(200);
expect(res.body.affected).toBe(2);
const list = await request(app).get('/api/sessions?filter=all');
expect(list.body.every(s => s.archived === 0)).toBe(true);
});
test('bulk delete multiple sessions', async () => {
const s1 = seedSession({ is_active: 0, notes: null });
const s2 = seedSession({ is_active: 0, notes: null });
const res = await request(app)
.post('/api/sessions/bulk')
.set('Authorization', getAuthHeader())
.send({ action: 'delete', ids: [s1.id, s2.id] });
expect(res.status).toBe(200);
expect(res.body.affected).toBe(2);
const list = await request(app).get('/api/sessions?filter=all');
expect(list.body).toHaveLength(0);
});
test('rejects archive of active sessions', async () => {
const active = seedSession({ is_active: 1, notes: null });
const closed = seedSession({ is_active: 0, notes: null });
const res = await request(app)
.post('/api/sessions/bulk')
.set('Authorization', getAuthHeader())
.send({ action: 'archive', ids: [active.id, closed.id] });
expect(res.status).toBe(400);
expect(res.body.activeIds).toContain(active.id);
const list = await request(app).get('/api/sessions?filter=all');
expect(list.body).toHaveLength(2);
expect(list.body.every(s => s.archived === 0)).toBe(true);
});
test('rejects delete of active sessions', async () => {
const active = seedSession({ is_active: 1, notes: null });
const res = await request(app)
.post('/api/sessions/bulk')
.set('Authorization', getAuthHeader())
.send({ action: 'delete', ids: [active.id] });
expect(res.status).toBe(400);
});
test('returns 400 for empty ids array', async () => {
const res = await request(app)
.post('/api/sessions/bulk')
.set('Authorization', getAuthHeader())
.send({ action: 'archive', ids: [] });
expect(res.status).toBe(400);
});
test('returns 400 for invalid action', async () => {
const res = await request(app)
.post('/api/sessions/bulk')
.set('Authorization', getAuthHeader())
.send({ action: 'nuke', ids: [1] });
expect(res.status).toBe(400);
});
test('returns 400 for non-array ids', async () => {
const res = await request(app)
.post('/api/sessions/bulk')
.set('Authorization', getAuthHeader())
.send({ action: 'archive', ids: 'not-array' });
expect(res.status).toBe(400);
});
test('returns 404 if any session ID does not exist', async () => {
const s1 = seedSession({ is_active: 0, notes: null });
const res = await request(app)
.post('/api/sessions/bulk')
.set('Authorization', getAuthHeader())
.send({ action: 'archive', ids: [s1.id, 9999] });
expect(res.status).toBe(404);
});
test('returns 401 without auth', async () => {
const res = await request(app)
.post('/api/sessions/bulk')
.send({ action: 'archive', ids: [1] });
expect(res.status).toBe(401);
});
});