feat: groupes de travail (P5)
Ajoute la notion de groupe — collection nommée de repos (membership légère persistée, many-to-many) — pour piloter plusieurs repos en simultané : - DB : migration #5 (tables groups + group_repos, FK ON DELETE CASCADE). - GroupManager (synchrone, EventEmitter) + routes REST /api/v1/groups ; ne persiste que la membership, worktrees/sessions filtrés par repoId côté client. - Protocole : GroupSummary, DTOs, topic WS « groups », events group_update/removed (additifs, PROTOCOL_VERSION inchangé). - Web : store groups, GroupsListView, GroupView (réutilise RepoSection), grille multi-terminaux (TerminalGrid/TerminalCell), action « feature cross-repo » (orchestration client, tolérante aux échecs partiels), routes + i18n EN/FR. - Tests : group-manager.test.ts + extension protocol.test.ts ; acceptance-p5.mjs.
This commit is contained in:
86
packages/server/src/routes/groups.ts
Normal file
86
packages/server/src/routes/groups.ts
Normal file
@@ -0,0 +1,86 @@
|
||||
import type { FastifyInstance } from 'fastify';
|
||||
import type {
|
||||
AddRepoRequest,
|
||||
CreateGroupRequest,
|
||||
GroupResponse,
|
||||
GroupsListResponse,
|
||||
UpdateGroupRequest,
|
||||
} from '@arboretum/shared';
|
||||
import type { GroupManager } from '../core/group-manager.js';
|
||||
import { sendManagerError } from './repos.js';
|
||||
|
||||
export function registerGroupRoutes(app: FastifyInstance, gm: GroupManager): void {
|
||||
app.get('/api/v1/groups', async (): Promise<GroupsListResponse> => ({ 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<CreateGroupRequest> | 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 } : {}),
|
||||
});
|
||||
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<UpdateGroupRequest> | 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 } : {}),
|
||||
});
|
||||
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' } });
|
||||
}
|
||||
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<AddRepoRequest> | 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);
|
||||
}
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user