Skip to content

moooon

Motir

Vibe your whole project. Bring an idea — Motir's three AI layers plan it, track it, and ship it, end to end. You're looking at Motir, built in Motir.

  • Vibe Project
  • Open Source
  • AI Agent
  • AI Loop
1
requests
0
upvotes
145
planned
1,361
shipped

Motir · Work items

MOTIR-284Done

5.3.3 `customFieldsService` (values) — per-type validated set/clear + revision-trail diffs + the bounded detail-read join

Estimate: 28m · Depends on: 5.3.1

The values half. setValue(workItemId, fieldId, rawValue | null, ctx) + the read shape the rail consumes.

Per-type validation (the service is the authority): text → trimmed, length-capped (a constant, e.g. 1000 chars); number → decimal parse into the Decimal column (reject NaN/∞; preserve scale); date → date-only ISO, UTC-safe (the dueDate T00:00:00.000Z convention — no local-tz off-by-one); select → the option belongs to this field AND is not archived for NEW writes (an existing value holding an archived option remains valid + renderable); user → a workspace member who can view the project (the 6.4 assignableMembersService scoping — same rule as assignee/mentions). Type mismatches and cross-field options → typed 422s.

Write semantics: non-null → upsert the [workItemId, fieldId] row (exactly one per-type column populated, the rest null); null → DELETE the row (no tombstones). In the SAME transaction, write the 1.4.6 revision entry — diff: { "customFields.<key>": { from, to } } (display-friendly from/to: the option label, the user id, the raw scalar — the mapper renders), changeKind: 'updated' — the History entry 5.5 renders, the parity Jira has. Permissions: who edits the issue edits values (admin/member with view; viewer 403; cross-workspace 404).

Read — extend getIssueDetail with customFields: CustomFieldWithValueDto[] (definitions in position order, each with its option set + this issue's value, resolved for display: option label+archived flag, user id/name/image). ONE bounded query (≤50 defs cap) slotted into the existing parallel fetch — no N+1, no separate round-trip from the page. A dedicated setCustomFieldValueAction server action (the rail pattern: action → service → router.refresh()); custom values do NOT overload updateIssueAction's scalar input (different table, different validation surface — keep the seams clean).

Acceptance criteria

  • setValue validates per type exactly as specified (incl. archived-option new-write rejection, non-viewable-user rejection, decimal + UTC-date integrity), upserts/deletes the single row, and writes the customFields.<key> revision diff in the same tx.
  • getIssueDetail returns the bounded customFields array (defs + resolved values) without an N+1; an issue with no values still lists definitions (the rail needs them for "Show more fields").
  • Permission matrix enforced (member sets, viewer 403, cross-workspace 404); concurrent set on the same pair converges via the upsert (last-write-wins, no duplicate-row error).
  • Routes/action are HTTP-only thin; pnpm test:coverage ≥90% across every type branch + the clear path.

Context refs

  • 5.3.1 value model + repos; lib/services/workItemsService.ts getIssueDetail (the parallel-fetch assembly to extend) + updateIssueAction (the action pattern to mirror, NOT extend)
  • lib/services/workItemRevisionsService.ts + the revision diff shape (Record<string, {from,to}>) — extend with the customFields.<key> keys
  • lib/services/assignableMembersService.ts (6.4) — the user-value scoping
  • The dueDate UTC convention (2.3.12) for the date type