3.6.2 Column config service + API — add/rename/reorder/delete column; map/unmap status
Estimate: 30m · Depends on: 3.1.1, 3.3.3
The write path for board-column administration, through the 4-layer architecture (Route → Service → Repository → Prisma), extending the Story-3.3 board-config seam rather than adding a new service or table.
Service (boardsService, the 3.3 config home). Add: addColumn(boardId, { name, position? }), renameColumn(columnId, name), reorderColumn(columnId, position) (fractional-index between neighbours, the same rank scheme work items use), deleteColumn(columnId), mapStatusToColumn(boardId, columnId, statusId), unmapStatus(boardId, statusId), and renameBoard(boardId, name). Each is ONE transaction, returns a DTO (extend lib/dto/boards.ts + lib/mappers/boardMappers.ts), throws typed errors (lib/boards/errors.ts), and calls assertProjectAdmin (membership gate, TODO(6.4), mirroring setColumnWipLimit / the 2.2.5 workflow writes).
The mapping is a MOVE, not a duplicate. board_column_status has @@unique([boardId, statusId]) (3.1.1). mapStatusToColumn therefore, in one $transaction: boardColumnStatusRepository.deleteByStatus(statusId, tx) then .create({ columnId, statusId }, tx) — so re-mapping a status replaces its row; a P2002 backstop covers the concurrent race (mirror createStatus).
Delete-column safety. deleteColumn unmaps the column’s statuses (deleteByColumn) — they return to unmappedStatuses — then deletes the board_column row. It never deletes a work item (a card’s column is derived from its status). Decide + implement the Jira-style guard: refuse to delete a column whose mapped status still holds board cards (ColumnNotEmptyError → 409) unless those statuses are remapped first; the last column / a column mapping the INITIAL status may need extra guards (decide from the mirror).
Routes (HTTP-only, one service call each, typed-error → status): POST /api/board/columns (add), extend PATCH /api/board/columns/[columnId] (3.3.3 — add name / position alongside the existing wipLimit), DELETE /api/board/columns/[columnId], PUT /api/board/columns/[columnId]/statuses (set the column’s mapped statuses) + DELETE .../statuses/[statusId] (unmap), and extend PATCH /api/board (3.3.3 group-by) with name. A write from another workspace is 403/404 (the workspaceId gate, finding #26). No Prisma/transaction in any route.
Acceptance criteria
boardsServicegainsaddColumn/renameColumn/reorderColumn/deleteColumn/mapStatusToColumn/unmapStatus/renameBoard, each one transaction, returning DTOs, throwing typed errors, gated byassertProjectAdmin(TODO(6.4)).mapStatusToColumnis a MOVE: a status maps to exactly one column per board (the@@unique([boardId, statusId])invariant holds; re-mapping replaces, never duplicates), proven by a vitest test.deleteColumnunmaps its statuses (they reappear inunmappedStatuses) and deletes NO work item; the empty/guard rule (a column whose status still holds cards) behaves per the chosen mirror-product semantics with a typed error.- Routes are HTTP-only (no Prisma, no
$transaction), map typed errors to status codes, and reject cross-workspace + non-admin writes (403/404); the existing WIP-limit + group-by behaviour of the extended routes is unchanged. - Repository writes go through
boardColumnRepository/boardColumnStatusRepository(add aboardColumnRepository.deleteif missing) with requiredtx; no new table / migration. - Vitest (real Postgres) covers add/rename/reorder/delete/map/unmap, the unique-move, the delete guard, and the workspace + membership gates.
Context refs
lib/services/boardsService.ts—setColumnWipLimit/setSwimlaneGroupBy(3.3.3): the config-write home +assertProjectAdminpattern to extendlib/repositories/boardColumnRepository.ts(create/update/findById) +boardColumnStatusRepository.ts(create/deleteByColumn/deleteByStatus) — the repos to reuse / extend (add columndelete)app/api/board/columns/[columnId]/route.ts(PATCH wipLimit, 3.3.3) +app/api/board/route.ts(PATCH group-by) — the routes to extend- Story 3.1.1 — the
board/board_column/board_column_statusschema +@@unique([boardId, statusId]);lib/workflows/*for the status source lib/services/workflowsService.tscreateStatus— the P2002-backstop +assertProjectAdminprecedent;motir-core/CLAUDE.md(4-layer rules)