5.3.2 `customFieldsService` (definitions) — CRUD + option rename/reorder/archive/delete-when-unused + the 50/55 caps, admin-gated, routes
Estimate: 30m · Depends on: 5.3.1
The definitions half of the service. Per the 4-layer rule: lib/services/customFieldsService.ts + typed errors (lib/customFields/errors.ts) + DTOs/mappers + HTTP-only routes.
Definition CRUD — createField(projectId, { label, fieldType, description?, options? }, ctx) (slug-generates the immutable key, unique per project; seeds initial options for select; enforces the 50-field cap with a typed error), renameField, reorderField (fractional index — the board-settings precedent), deleteField (the team-managed mirror: HARD delete, values destroyed via the cascade; the service returns/exposes the value count so the UI confirm can name it — countValuesByField read first). All project-admin-gated — the 6.4 two-tier check (isWorkspaceManager(wsRole) || projectMembership.role === admin), exactly the members-page pattern.
Option management (select fields) — addOption (55-cap), renameOption, reorderOption, and the verified split: archiveOption (any time — hidden from new selection, existing values keep rendering) vs deleteOption (only when unused — countValuesByOption === 0, else a typed OptionInUseError; the DB Restrict backstops). Unarchive supported (the inverse is free).
Reads — listFields(projectId, ctx) (admin page: definitions + option sets + per-field value counts, ≤50 bounded); a lighter listFieldsForIssueRail shape is 5.3.3's concern. Routes: GET/POST /api/projects/[id]/fields, PATCH/DELETE /api/fields/[id], POST/PATCH/DELETE under /api/fields/[id]/options — parse → one service call → typed-error mapping (403 not-admin / 404 cross-workspace per finding #44 / 409 in-use / 422 caps).
Acceptance criteria
- Field create/rename/reorder/delete + option add/rename/reorder/archive/unarchive/delete ship with the caps (50/55 → typed errors), the immutable-key rule, and the only-when-unused option delete (in-use → 409; archive offered); field delete cascades values and the API exposes the pre-delete value count.
- Every mutation is project-admin-gated (6.4 two-tier); non-admins get 403, cross-workspace 404; a
viewer/membercan READ definitions (the rail needs them) but not mutate. - One service method = one transaction; reorder uses fractional indexing (no renumber sweeps); routes are HTTP-only;
pnpm test:coverage≥90% incl. the cap + in-use branches.
Context refs
- 5.3.1 models/repos;
app/(authed)/settings/project/members/page.tsx(the 6.4 admin-gate pattern to reuse) +lib/projects/roles - The board-settings fractional reorder precedent (3.6) for option/field reorder
- The verified mirror rules in the Story 5.3 description (50/55 caps; hard field delete; archive vs delete-when-unused)
lib/services/workspacesService.ts/projectsServiceDTO+mapper conventions