import type { FastifyInstance } from 'fastify'; import type { AddRepoRequest, CreateGroupRequest, GroupResponse, GroupsListResponse, UpdateGroupRequest, } from '@arboretum/shared'; import type { GroupManager } from '../core/group-manager.js'; import type { Db } from '../db/index.js'; import { recordAudit } from '../core/audit-log.js'; import { sendManagerError } from './repos.js'; export function registerGroupRoutes(app: FastifyInstance, gm: GroupManager, db: Db): void { app.get('/api/v1/groups', async (): Promise => ({ groups: gm.listGroups() })); app.get('/api/v1/groups/:id', async (req, reply) => { const { id } = req.params as { id: string }; try { return reply.send({ group: gm.getGroup(id) } satisfies GroupResponse); } catch (err) { return sendManagerError(reply, err); } }); app.post('/api/v1/groups', async (req, reply) => { const body = req.body as Partial | null; if (!body || typeof body.label !== 'string') { return reply.status(400).send({ error: { code: 'BAD_REQUEST', message: 'label is required' } }); } try { const group = gm.createGroup({ label: body.label, ...(body.description !== undefined ? { description: body.description } : {}), ...(body.color !== undefined ? { color: body.color } : {}), ...(Array.isArray(body.repoIds) ? { repoIds: body.repoIds } : {}), }); recordAudit(db, { actor: req.authContext?.tokenId ?? 'unknown', action: 'group.create', resourceId: group.id, details: { label: group.label } }); return reply.status(201).send({ group } satisfies GroupResponse); } catch (err) { return sendManagerError(reply, err); } }); app.patch('/api/v1/groups/:id', async (req, reply) => { const { id } = req.params as { id: string }; const body = (req.body as Partial | null) ?? {}; try { const group = gm.updateGroup(id, { ...(body.label !== undefined ? { label: body.label } : {}), ...(body.description !== undefined ? { description: body.description } : {}), ...(body.color !== undefined ? { color: body.color } : {}), }); recordAudit(db, { actor: req.authContext?.tokenId ?? 'unknown', action: 'group.update', resourceId: id }); return reply.send({ group } satisfies GroupResponse); } catch (err) { return sendManagerError(reply, err); } }); app.delete('/api/v1/groups/:id', async (req, reply) => { const { id } = req.params as { id: string }; if (!gm.deleteGroup(id)) { return reply.status(404).send({ error: { code: 'NOT_FOUND', message: 'No group with this id' } }); } recordAudit(db, { actor: req.authContext?.tokenId ?? 'unknown', action: 'group.delete', resourceId: id }); return reply.send({ ok: true }); }); app.post('/api/v1/groups/:id/repos', async (req, reply) => { const { id } = req.params as { id: string }; const body = req.body as Partial | null; if (!body || typeof body.repoId !== 'string') { return reply.status(400).send({ error: { code: 'BAD_REQUEST', message: 'repoId is required' } }); } try { return reply.send({ group: gm.addRepo(id, body.repoId) } satisfies GroupResponse); } catch (err) { return sendManagerError(reply, err); } }); app.delete('/api/v1/groups/:id/repos/:repoId', async (req, reply) => { const { id, repoId } = req.params as { id: string; repoId: string }; try { return reply.send({ group: gm.removeRepo(id, repoId) } satisfies GroupResponse); } catch (err) { return sendManagerError(reply, err); } }); }